Compare commits

...

9 Commits

21 changed files with 1030 additions and 393 deletions

3
.gitignore vendored
View File

@ -1 +1,2 @@
node_modules
node_modules/

View File

@ -0,0 +1,41 @@
.fc-media-screen {
flex-grow: 1;
height: 74vh;
}
.fc-prev-button,
.fc-next-button {
background-color: var(--color-blue-600) !important;
border: none !important;
transition: 0.2s ease;
}
.fc-prev-button:hover,
.fc-next-button:hover {
background-color: var(--color-blue-700) !important;
}
.fc-timeGridWeek-button,
.fc-timeGridDay-button,
.fc-dayGridMonth-button {
border: none !important;
background-color: var(--color-blue-600) !important;
transition: 0.2s ease;
}
.fc-timeGridWeek-button:hover,
.fc-timeGridDay-button:hover,
.fc-dayGridMonth-button:hover {
background-color: var(--color-blue-700) !important;
}
.fc-button-active {
background-color: var(--color-blue-500) !important;
}
.fc-toolbar-title {
font-weight: bold;
color: var(--color-gray-900);
}

View File

@ -0,0 +1,174 @@
"use client";
import { useEffect, useState } from "react";
import dynamic from "next/dynamic";
import pt_br_locale from "@fullcalendar/core/locales/pt-br";
import FullCalendar from "@fullcalendar/react";
import dayGridPlugin from "@fullcalendar/daygrid";
import interactionPlugin from "@fullcalendar/interaction";
import timeGridPlugin from "@fullcalendar/timegrid";
import { EventInput } from "@fullcalendar/core/index.js";
import { Sidebar } from "@/components/dashboard/sidebar";
import { PagesHeader } from "@/components/dashboard/header";
import { Button } from "@/components/ui/button";
import {
mockAppointments,
mockWaitingList,
} from "@/lib/mocks/appointment-mocks";
import "./index.css";
import Link from "next/link";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
const ListaEspera = dynamic(
() => import("@/components/agendamento/ListaEspera"),
{ ssr: false }
);
export default function AgendamentoPage() {
const [appointments, setAppointments] = useState(mockAppointments);
const [waitingList, setWaitingList] = useState(mockWaitingList);
const [activeTab, setActiveTab] = useState<"calendar" | "espera">("calendar");
const [requestsList, setRequestsList] = useState<EventInput[]>();
useEffect(() => {
document.addEventListener("keydown", (event) => {
if (event.key === "c") {
setActiveTab("calendar");
}
if (event.key === "f") {
setActiveTab("espera");
}
});
}, []);
useEffect(() => {
let events: EventInput[] = [];
appointments.forEach((obj) => {
const event: EventInput = {
title: `${obj.patient}: ${obj.type}`,
start: new Date(obj.time),
end: new Date(new Date(obj.time).getTime() + obj.duration * 60 * 1000),
color:
obj.status === "confirmed"
? "#68d68a"
: obj.status === "pending"
? "#ffe55f"
: "#ff5f5fff",
};
events.push(event);
});
setRequestsList(events);
}, [appointments]);
// mantive para caso a lógica de salvar consulta passe a funcionar
const handleSaveAppointment = (appointment: any) => {
if (appointment.id) {
setAppointments((prev) =>
prev.map((a) => (a.id === appointment.id ? appointment : a))
);
} else {
const newAppointment = {
...appointment,
id: Date.now().toString(),
};
setAppointments((prev) => [...prev, newAppointment]);
}
};
const handleNotifyPatient = (patientId: string) => {
console.log(`Notificando paciente ${patientId}`);
};
return (
<div className="flex flex-row">
<div className="flex w-full flex-col">
<div className="flex w-full flex-col gap-10 p-6">
<div className="flex flex-row justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-foreground">{activeTab === "calendar" ? "Calendário" : "Lista de Espera"}</h1>
<p className="text-muted-foreground">
Navegue através dos atalhos: Calendário (C) ou Fila de espera
(F).
</p>
</div>
<div className="flex space-x-2">
{/* <Link href={"/agenda"}>
<Button className="bg-blue-600 hover:bg-blue-700">
Agenda
</Button>
</Link> */}
<DropdownMenu>
<DropdownMenuTrigger className="bg-blue-600 hover:bg-blue-700 px-5 py-1 text-white rounded-sm">
Opções &#187;
</DropdownMenuTrigger>
<DropdownMenuContent>
<Link href={"/agenda"}>
<DropdownMenuItem>Agendamento</DropdownMenuItem>
</Link>
<Link href={"/procedimento"}>
<DropdownMenuItem>Procedimento</DropdownMenuItem>
</Link>
<Link href={"/financeiro"}>
<DropdownMenuItem>Financeiro</DropdownMenuItem>
</Link>
</DropdownMenuContent>
</DropdownMenu>
<div className="flex flex-row">
<Button
variant={"outline"}
className="bg-gray-100 hover:bg-gray-200 hover:text-foreground rounded-l-[100px] rounded-r-[0px]"
onClick={() => setActiveTab("calendar")}
>
Calendário
</Button>
<Button
variant={"outline"}
className="bg-gray-100 hover:bg-gray-200 hover:text-foreground rounded-r-[100px] rounded-l-[0px]"
onClick={() => setActiveTab("espera")}
>
Lista de espera
</Button>
</div>
</div>
</div>
{activeTab === "calendar" ? (
<div className="flex w-full">
<FullCalendar
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
initialView="dayGridMonth"
locale={pt_br_locale}
events={requestsList}
headerToolbar={{
left: "prev,next today",
center: "title",
right: "dayGridMonth,timeGridWeek,timeGridDay",
}}
dateClick={(info) => {
info.view.calendar.changeView("timeGridDay", info.dateStr);
}}
selectable={true}
selectMirror={true}
dayMaxEvents={true}
dayMaxEventRows={3}
/>
</div>
) : (
<ListaEspera
patients={waitingList}
onNotify={handleNotifyPatient}
onAddToWaitlist={() => {}}
/>
)}
</div>
</div>
</div>
);
}

View File

@ -140,7 +140,7 @@ export default function PacientesPage() {
if (showForm) {
return (
<div className="space-y-6">
<div className="space-y-6 p-6">
<div className="flex items-center gap-4">
<Button variant="ghost" onClick={() => setShowForm(false)}>
<ArrowLeft className="h-4 w-4" />
@ -160,7 +160,7 @@ export default function PacientesPage() {
}
return (
<div className="space-y-6">
<div className="space-y-6 p-6">
{}
<div className="flex items-center justify-between gap-4 flex-wrap">
<div>

View File

@ -0,0 +1,41 @@
export default function DashboardPage() {
return (
<>
<div className="space-y-6 p-6">
<div>
<h1 className="text-2xl font-bold text-foreground">Dashboard</h1>
<p className="text-muted-foreground">
Bem-vindo ao painel de controle
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="bg-card p-6 rounded-lg border">
<h3 className="text-sm font-medium text-muted-foreground">
Total de Pacientes
</h3>
<p className="text-2xl font-bold text-foreground">1,234</p>
</div>
<div className="bg-card p-6 rounded-lg border">
<h3 className="text-sm font-medium text-muted-foreground">
Consultas Hoje
</h3>
<p className="text-2xl font-bold text-foreground">28</p>
</div>
<div className="bg-card p-6 rounded-lg border">
<h3 className="text-sm font-medium text-muted-foreground">
Próximas Consultas
</h3>
<p className="text-2xl font-bold text-foreground">45</p>
</div>
<div className="bg-card p-6 rounded-lg border">
<h3 className="text-sm font-medium text-muted-foreground">
Receita Mensal
</h3>
<p className="text-2xl font-bold text-foreground">R$ 45.230</p>
</div>
</div>
</div>
</>
);
}

View File

@ -0,0 +1,22 @@
import type React from "react";
import { Sidebar } from "@/components/dashboard/sidebar";
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
import { PagesHeader } from "@/components/dashboard/header";
export default function MainRoutesLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen bg-background flex">
<SidebarProvider>
<Sidebar />
<main className="flex-1">
<PagesHeader />
{children}
</main>
</SidebarProvider>
</div>
);
}

View File

@ -0,0 +1,373 @@
"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 HeaderAgenda from "@/components/agenda/HeaderAgenda";
import FooterAgenda from "@/components/agenda/FooterAgenda";
export default function NovoAgendamentoPage() {
const [bloqueio, setBloqueio] = useState(false);
return (
// ====== WRAPPER COM ESPAÇAMENTO GERAL ======
<div className="min-h-screen flex flex-col bg-white">
{/* HEADER fora do <main>, usando o MESMO container do footer */}
<HeaderAgenda />
{/* Conteúdo */}
<main className="flex-1 mx-auto w-full max-w-7xl px-8 py-8 space-y-8">
{/* ==== INFORMAÇÕES DO PACIENTE — layout idêntico ao print ==== */}
<div className="border rounded-md p-6 space-y-4 bg-white">
<h2 className="font-medium">Informações do paciente</h2>
{/* grade principal: 12 colunas para controlar as larguras */}
<div className="grid grid-cols-1 md:grid-cols-12 gap-4">
{/* ===== Linha 1 ===== */}
<div className="md:col-span-6">
<Label>Nome *</Label>
<div className="relative">
<Search className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
placeholder="Digite o nome do paciente"
className="h-10 pl-8"
/>
</div>
</div>
<div className="md:col-span-3">
<Label>CPF do paciente</Label>
<Input placeholder="Número do CPF" className="h-10" />
</div>
<div className="md:col-span-3">
<Label>RG</Label>
<Input placeholder="Número do RG" className="h-10" />
</div>
{/* ===== Linha 2 ===== */}
{/* 1ª coluna (span 6) com sub-grid: Data (5 col) + Telefone (7 col) */}
<div className="md:col-span-6">
<div className="grid grid-cols-12 gap-3">
<div className="col-span-5">
<Label>Data de nascimento *</Label>
<Input type="date" className="h-10" />
</div>
<div className="col-span-7">
<Label>Telefone</Label>
<div className="grid grid-cols-[86px_1fr] gap-2">
<select className="h-10 rounded-md border border-input bg-background px-2 text-[13px]">
<option value="+55">+55</option>
<option value="+351">+351</option>
<option value="+1">+1</option>
</select>
<Input placeholder="(99) 99999-9999" className="h-10" />
</div>
</div>
</div>
</div>
{/* 2ª coluna da linha 2: E-mail (span 6) */}
<div className="md:col-span-6">
<Label>E-mail</Label>
<Input
type="email"
placeholder="email@exemplo.com"
className="h-10"
/>
</div>
{/* ===== Linha 3 ===== */}
<div className="md:col-span-6">
<Label>Convênio</Label>
<div className="relative">
<select
defaultValue="particular"
className="h-10 w-full rounded-md border border-input bg-background pr-8 pl-3 text-[13px] appearance-none"
>
<option value="particular">Particular</option>
<option value="plano-a">Plano A</option>
<option value="plano-b">Plano B</option>
</select>
<ChevronDown className="pointer-events-none absolute right-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
</div>
</div>
<div className="md:col-span-6 grid grid-cols-2 gap-4">
<div>
<Label>Matrícula</Label>
<Input defaultValue="000000000" className="h-10" />
</div>
<div>
<Label>Validade</Label>
<Input placeholder="00/0000" className="h-10" />
</div>
</div>
</div>
{/* link Informações adicionais */}
<button
type="button"
className="text-sm text-blue-600 inline-flex items-center gap-1 hover:underline"
aria-label="Ver informações adicionais do paciente"
>
Informações adicionais
<ChevronDown className="h-4 w-4" aria-hidden="true" />
</button>
{/* barra Documentos e anexos */}
<div className="flex items-center justify-between border rounded-md px-3 py-2">
<span className="text-sm">Documentos e anexos</span>
<div className="flex gap-2">
<Button
size="icon"
variant="outline"
className="h-8 w-8"
aria-label="Enviar documento"
>
<Upload className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="outline"
className="h-8 w-8"
aria-label="Baixar documento"
>
<FileDown className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="outline"
className="h-8 w-8"
aria-label="Gerenciar etiquetas"
>
<Tag className="h-4 w-4" />
</Button>
</div>
</div>
</div>
{/* ==== INFORMAÇÕES DO ATENDIMENTO ==== */}
<div className="border rounded-md p-6 space-y-4 bg-white">
<h2 className="font-medium">Informações do atendimento</h2>
{/* GRID PRINCIPAL: 12 colunas */}
<div className="grid grid-cols-1 md:grid-cols-12 gap-6">
{/* COLUNA ESQUERDA (span 6) */}
<div className="md:col-span-6 space-y-3">
{/* Nome do profissional */}
<div>
<Label className="text-[13px]">
Nome do profissional <span className="text-red-600">*</span>
</Label>
<div className="relative">
<Search className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<input
defaultValue="Robson Alves dos Anjos Neto"
className="h-10 w-full rounded-full border border-input pl-8 pr-12 text-[13px] focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
<span className="absolute right-2 top-1/2 -translate-y-1/2 inline-flex h-6 min-w-[28px] items-center justify-center rounded-full bg-muted px-2 text-xs font-medium">
RA
</span>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
{/* Unidade */}
<div>
<Label className="text-[13px]">
Unidade <span className="text-red-600">*</span>
</Label>
<select
defaultValue="nei"
className="h-10 w-full rounded-md border border-input bg-background pr-8 pl-3 text-[13px] appearance-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="nei">
Núcleo de Especialidades Integradas
</option>
<option value="cc">Clínica Central</option>
</select>
</div>
{/* Data com ícone */}
<div>
<Label className="text-[13px]">
Data <span className="text-red-600">*</span>
</Label>
<div className="relative">
<Calendar className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<input
type="date"
defaultValue="2025-07-29"
className="h-10 w-full rounded-md border border-input pl-8 pr-3 text-[13px] focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
</div>
</div>
{/* Início / Término / Profissional solicitante (na mesma linha) */}
<div className="grid grid-cols-12 gap-3 items-end">
{/* Início (maior) */}
<div className="col-span-12 md:col-span-3">
<Label className="text-[13px]">
Início <span className="text-red-600">*</span>
</Label>
<input
type="time"
defaultValue="21:03"
className="h-10 w-full rounded-md border border-input px-3 text-[13px] focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
{/* Término (maior) */}
<div className="col-span-12 md:col-span-3">
<Label className="text-[13px]">
Término <span className="text-red-600">*</span>
</Label>
<input
type="time"
defaultValue="21:03"
className="h-10 w-full rounded-md border border-input px-3 text-[13px] focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
{/* Profissional solicitante */}
<div className="col-span-12 md:col-span-6">
<Label className="text-[13px]">
Profissional solicitante
</Label>
<div className="relative">
{/* ícone de busca à esquerda */}
<svg
className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
<select
defaultValue=""
className="h-10 w-full rounded-md border border-input bg-background pl-8 pr-8 text-[13px] appearance-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="" disabled>
Selecione o solicitante
</option>
<option value="1">Dr. Carlos Silva</option>
<option value="2">Dra. Maria Santos</option>
</select>
<svg
className="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M6 9l6 6 6-6" />
</svg>
</div>
</div>
</div>
</div>
{/* COLUNA DIREITA — altura/posição como a imagem 1 */}
<div className="md:col-span-6 relative -top-10">
{/* toolbar */}
<div className="mb-2 flex items-center justify-end gap-1">
<Button size="icon" variant="outline" className="h-8 w-8">
<Accessibility className="h-4 w-4" />
</Button>
<Button size="icon" variant="outline" className="h-8 w-8">
<Volume2 className="h-4 w-4" />
</Button>
<Button size="icon" variant="outline" className="h-8 w-8">
<Flame className="h-4 w-4" />
</Button>
<Button size="icon" variant="outline" className="h-8 w-8">
<Settings className="h-4 w-4" />
</Button>
<Button size="icon" variant="outline" className="h-8 w-8">
<Clipboard className="h-4 w-4" />
</Button>
</div>
{/* Tipo de atendimento + campo de busca */}
<div className="mb-2">
<div className="flex items-center justify-between">
<Label className="text-[13px]">
Tipo de atendimento <span className="text-red-600">*</span>
</Label>
<label className="flex items-center gap-1 text-xs text-muted-foreground">
<input
type="checkbox"
className="h-3.5 w-3.5 accent-current"
/>{" "}
Pagamento via Reembolso
</label>
</div>
<div className="relative mt-1">
<Search className="pointer-events-none absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="Pesquisar"
className="h-10 w-full rounded-md border border-input pl-8 pr-8 text-[13px]"
/>
<ChevronDown className="pointer-events-none absolute right-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
</div>
</div>
{/* Observações + imprimir */}
<div className="mb-0.2 flex items-center justify-between">
<Label className="text-[13px]">Observações</Label>
<label className="flex items-center gap-2 text-xs text-muted-foreground">
<input
type="checkbox"
className="h-3.5 w-3.5 accent-current"
/>{" "}
Imprimir na Etiqueta / Pulseira
</label>
</div>
{/* Textarea mais baixo e compacto */}
<Textarea
rows={4}
placeholder=""
className="text-[13px] h-[110px] min-h-0 resize-none"
/>
</div>
</div>
</div>
</main>
{/* ====== FOOTER FIXO ====== */}
<FooterAgenda />
</div>
);
}

View File

@ -1,139 +0,0 @@
'use client';
import { useState } from 'react';
import dynamic from 'next/dynamic';
const AgendaCalendar = dynamic(() => import('@/components/agendamento/AgendaCalendar'), {
ssr: false,
loading: () => (
<div className="bg-white rounded-lg shadow p-6">
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/4 mb-4"></div>
<div className="h-4 bg-gray-200 rounded w-1/2 mb-6"></div>
<div className="space-y-4">
<div className="h-12 bg-gray-200 rounded"></div>
<div className="h-64 bg-gray-200 rounded"></div>
</div>
</div>
</div>
)
});
const AppointmentModal = dynamic(() => import('@/components/agendamento/AppointmentModal'), { ssr: false });
const ListaEspera = dynamic(() => import('@/components/agendamento/ListaEspera'), { ssr: false });
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: '2', patient: 'Pedro Alves', time: '2025-09-10T10:30', duration: 45, type: 'retorno' as const, status: 'pending' as const, professional: '2', notes: '' },
{ id: '3', patient: 'Mariana Lima', time: '2025-09-10T14:00', duration: 60, type: 'exame' as const, status: 'confirmed' as const, professional: '3', notes: '' },
];
const mockWaitingList = [
{ id: '1', name: 'Ana Costa', specialty: 'Cardiologia', preferredDate: '2025-09-12', priority: 'high' as const, contact: '(11) 99999-9999' },
{ id: '2', name: 'Pedro Alves', specialty: 'Dermatologia', preferredDate: '2025-09-15', priority: 'medium' as const, contact: '(11) 98888-8888' },
{ id: '3', name: 'Mariana Lima', specialty: 'Ortopedia', preferredDate: '2025-09-20', priority: 'low' as const, contact: '(11) 97777-7777' },
];
const mockProfessionals = [
{ id: '1', name: 'Dr. Carlos Silva', specialty: 'Cardiologia' },
{ id: '2', name: 'Dra. Maria Santos', specialty: 'Dermatologia' },
{ id: '3', name: 'Dr. João Oliveira', specialty: 'Ortopedia' },
];
export default function AgendamentoPage() {
const [appointments, setAppointments] = useState(mockAppointments);
const [waitingList, setWaitingList] = useState(mockWaitingList);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedAppointment, setSelectedAppointment] = useState<any>(null);
const [activeTab, setActiveTab] = useState<'agenda' | 'espera'>('agenda');
const handleSaveAppointment = (appointment: any) => {
if (appointment.id) {
setAppointments(prev => prev.map(a => a.id === appointment.id ? appointment : a));
} else {
const newAppointment = {
...appointment,
id: Date.now().toString(),
};
setAppointments(prev => [...prev, newAppointment]);
}
};
const handleEditAppointment = (appointment: any) => {
setSelectedAppointment(appointment);
setIsModalOpen(true);
};
const handleAddAppointment = () => {
setSelectedAppointment(null);
setIsModalOpen(true);
};
const handleCloseModal = () => {
setIsModalOpen(false);
setSelectedAppointment(null);
};
const handleNotifyPatient = (patientId: string) => {
console.log(`Notificando paciente ${patientId}`);
};
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold text-gray-900">Agendamento</h1>
<p className="text-gray-600 mt-2">Gerencie a agenda da clínica</p>
</div>
<div className="flex space-x-2">
<button
onClick={() => setActiveTab('agenda')}
className={`px-4 py-2 rounded-md ${
activeTab === 'agenda'
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-700'
}`}
>
Agenda
</button>
<button
onClick={() => setActiveTab('espera')}
className={`px-4 py-2 rounded-md ${
activeTab === 'espera'
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-700'
}`}
>
Lista de Espera
</button>
</div>
</div>
{activeTab === 'agenda' ? (
<AgendaCalendar
professionals={mockProfessionals}
appointments={appointments}
onAddAppointment={handleAddAppointment}
onEditAppointment={handleEditAppointment}
/>
) : (
<ListaEspera
patients={waitingList}
onNotify={handleNotifyPatient}
onAddToWaitlist={() => {}}
/>
)}
<AppointmentModal
isOpen={isModalOpen}
onClose={handleCloseModal}
onSave={handleSaveAppointment}
professionals={mockProfessionals}
appointment={selectedAppointment}
/>
</div>
);
}

View File

@ -1,19 +0,0 @@
import type React from "react"
import { Sidebar } from "@/components/dashboard/sidebar"
import { DashboardHeader } from "@/components/dashboard/header"
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="min-h-screen bg-background flex">
<Sidebar />
<div className="flex-1 flex flex-col">
<DashboardHeader />
<main className="flex-1 p-6">{children}</main>
</div>
</div>
)
}

View File

@ -1,29 +0,0 @@
export default function DashboardPage() {
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-foreground">Dashboard</h1>
<p className="text-muted-foreground">Bem-vindo ao painel de controle</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="bg-card p-6 rounded-lg border">
<h3 className="text-sm font-medium text-muted-foreground">Total de Pacientes</h3>
<p className="text-2xl font-bold text-foreground">1,234</p>
</div>
<div className="bg-card p-6 rounded-lg border">
<h3 className="text-sm font-medium text-muted-foreground">Consultas Hoje</h3>
<p className="text-2xl font-bold text-foreground">28</p>
</div>
<div className="bg-card p-6 rounded-lg border">
<h3 className="text-sm font-medium text-muted-foreground">Próximas Consultas</h3>
<p className="text-2xl font-bold text-foreground">45</p>
</div>
<div className="bg-card p-6 rounded-lg border">
<h3 className="text-sm font-medium text-muted-foreground">Receita Mensal</h3>
<p className="text-2xl font-bold text-foreground">R$ 45.230</p>
</div>
</div>
</div>
)
}

View File

@ -10,7 +10,7 @@
--card-foreground: #334155;
--popover: #ffffff;
--popover-foreground: #475569;
--primary: #0f766e;
--primary: var(--color-blue-500);
--primary-foreground: #ffffff;
--secondary: #e2e8f0;
--secondary-foreground: #475569;
@ -22,7 +22,7 @@
--destructive-foreground: #ffffff;
--border: #e2e8f0;
--input: #f1f5f9;
--ring: #0f766e;
--ring: var(--color-blue-500);
--chart-1: #0891b2;
--chart-2: #0f766e;
--chart-3: #f59e0b;
@ -31,47 +31,12 @@
--radius: 0.5rem;
--sidebar: #ffffff;
--sidebar-foreground: #475569;
--sidebar-primary: #0f766e;
--sidebar-primary: var(--color-blue-500);
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: #0891b2;
--sidebar-accent: var(--color-blue-500);
--sidebar-accent-foreground: #ffffff;
--sidebar-border: #e2e8f0;
--sidebar-ring: #0f766e;
}
.dark {
--background: #0f172a;
--foreground: #f1f5f9;
--card: #1e293b;
--card-foreground: #f1f5f9;
--popover: #1e293b;
--popover-foreground: #f1f5f9;
--primary: #14b8a6;
--primary-foreground: #0f172a;
--secondary: #334155;
--secondary-foreground: #f1f5f9;
--muted: #334155;
--muted-foreground: #94a3b8;
--accent: #0891b2;
--accent-foreground: #f1f5f9;
--destructive: #ef4444;
--destructive-foreground: #f1f5f9;
--border: #334155;
--input: #334155;
--ring: #14b8a6;
--chart-1: #0891b2;
--chart-2: #14b8a6;
--chart-3: #f59e0b;
--chart-4: #ef4444;
--chart-5: #94a3b8;
--sidebar: #1e293b;
--sidebar-foreground: #f1f5f9;
--sidebar-primary: #14b8a6;
--sidebar-primary-foreground: #0f172a;
--sidebar-accent: #0891b2;
--sidebar-accent-foreground: #f1f5f9;
--sidebar-border: #334155;
--sidebar-ring: #14b8a6;
--sidebar-ring: var(--color-blue-500);
}
@theme inline {

View File

@ -0,0 +1,89 @@
"use client";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Search, ChevronDown, RotateCcw } from "lucide-react";
import { Plus } from "lucide-react";
import HeaderAgenda from "@/components/agenda/HeaderAgenda";
import FooterAgenda from "@/components/agenda/FooterAgenda";
export default function ProcedimentoPage() {
const pathname = usePathname();
const router = useRouter();
const [bloqueio, setBloqueio] = useState(false);
const isAg = pathname?.startsWith("/agendamento");
const isPr = pathname?.startsWith("/procedimento");
const isFi = pathname?.startsWith("/financeiro");
const tab = (active: boolean, extra = "") =>
`px-4 py-1.5 text-[13px] border ${
active
? "border-sky-500 bg-sky-50 text-sky-700 font-medium"
: "text-gray-700 hover:bg-gray-100"
} ${extra}`;
return (
<div className="w-full min-h-screen flex flex-col bg-white">
{/* HEADER */}
<HeaderAgenda />
{/* CORPO */}
<main className="mx-auto w-full max-w-7xl px-8 py-6 space-y-6 flex-grow">
{/* ATENDIMENTOS */}
<section className="space-y-6">
{/* Selo Atendimento com + dentro da bolinha */}
<div className="inline-flex items-center gap-2 border px-3 py-1.5 bg-white text-[12px] rounded-md cursor-pointer hover:bg-gray-50">
<span className="flex h-5 w-5 items-center justify-center rounded-full border border-gray-400 bg-gray-100 text-gray-700">
<Plus className="h-3 w-3" strokeWidth={2} />
</span>
Atendimento
</div>
{/* Traço separador */}
<div className="border-b border-gray-200" />
{/* PROCEDIMENTOS */}
<div className="space-y-1">
<Label className="text-[13px] text-foreground/80">
Procedimentos
</Label>
<div className="relative">
<Search className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar"
className="h-10 w-full rounded-md pl-8 pr-8 border border-gray-300 focus-visible:ring-1 focus-visible:ring-sky-500 focus-visible:border-sky-500"
/>
<ChevronDown className="pointer-events-none absolute right-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
</div>
</div>
{/* Traço separador */}
<div className="border-b border-gray-200" />
{/* OUTRAS DESPESAS */}
<div className="space-y-1">
<Label className="text-[13px] text-foreground/80">
Outras despesas
</Label>
<div className="relative">
<Search className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar"
className="h-10 w-full rounded-md pl-8 pr-8 border border-gray-300 focus-visible:ring-1 focus-visible:ring-sky-500 focus-visible:border-sky-500"
/>
<ChevronDown className="pointer-events-none absolute right-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
</div>
</div>
</section>
</main>
{/* RODAPÉ FIXO */}
<FooterAgenda />
</div>
);
}

View File

@ -0,0 +1,34 @@
"use client";
import { Save } from "lucide-react";
import { Button } from "../ui/button";
import { Label } from "../ui/label";
import { Switch } from "../ui/switch";
import { useState } from "react";
import Link from "next/link";
export default function FooterAgenda() {
const [bloqueio, setBloqueio] = useState(false);
return (
<div className="sticky bottom-0 left-0 right-0 border-t bg-white">
<div className="mx-auto w-full max-w-7xl px-8 py-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<Switch checked={bloqueio} onCheckedChange={setBloqueio} />
<Label className="text-sm">Bloqueio de Agenda</Label>
</div>
<div className="flex gap-2">
<Link href={"/calendar"}>
<Button variant="ghost" className="hover:bg-blue-100 hover:text-foreground">Cancelar</Button>
</Link>
<Link href={"/calendar"}>
<Button>
<Save className="mr-2 h-4 w-4" />
Salvar as alterações
</Button>
</Link>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,71 @@
"use client";
import { RotateCcw } from "lucide-react";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
export default function HeaderAgenda() {
const pathname = usePathname();
const router = useRouter();
const isAg = pathname?.startsWith("/agendamento");
const isPr = pathname?.startsWith("/procedimento");
const isFi = pathname?.startsWith("/financeiro");
const tabCls = (active: boolean, extra = "") =>
`px-4 py-1.5 text-[13px] border ${
active
? "border-blue-500 bg-blue-50 text-blue-700 font-medium"
: "text-gray-700 hover:bg-gray-100"
} ${extra}`;
return (
<header className="border-b bg-white">
<div className="mx-auto w-full max-w-7xl px-8 py-3 flex items-center justify-between">
<h1 className="text-[18px] font-semibold">Novo Agendamento</h1>
<div className="flex items-center gap-2">
<nav
role="tablist"
aria-label="Navegação de Agendamento"
className="flex items-center gap-2"
>
<Link
href="/agenda"
role="tab"
aria-selected={isAg}
className={tabCls(Boolean(isAg)) + " rounded-md"}
>
Agendamento
</Link>
<Link
href="/procedimento"
role="tab"
aria-selected={isPr}
className={tabCls(Boolean(isPr)) + " rounded-md"}
>
Procedimento
</Link>
<Link
href="/financeiro"
role="tab"
aria-selected={isFi}
className={tabCls(Boolean(isFi)) + " rounded-md"}
>
Financeiro
</Link>
</nav>
<button
type="button"
aria-label="Histórico"
onClick={() => router.back()}
className="inline-flex h-8 w-8 items-center justify-center rounded-md border bg-white text-gray-700 hover:bg-gray-100"
>
<RotateCcw className="h-4 w-4" />
</button>
</div>
</div>
</header>
);
}

View File

@ -1,119 +0,0 @@
'use client';
import { useState } from 'react';
import { AgendaCalendar, AppointmentModal, ListaEspera } from '@/components/agendamento';
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: '2', patient: 'Pedro Alves', time: '2025-09-10T10:30', duration: 45, type: 'retorno' as const, status: 'pending' as const, professional: '2', notes: '' },
{ id: '3', patient: 'Mariana Lima', time: '2025-09-10T14:00', duration: 60, type: 'exame' as const, status: 'confirmed' as const, professional: '3', notes: '' },
];
const mockWaitingList = [
{ id: '1', name: 'Ana Costa', specialty: 'Cardiologia', preferredDate: '2025-09-12', priority: 'high' as const, contact: '(11) 99999-9999' },
{ id: '2', name: 'Pedro Alves', specialty: 'Dermatologia', preferredDate: '2025-09-15', priority: 'medium' as const, contact: '(11) 98888-8888' },
{ id: '3', name: 'Mariana Lima', specialty: 'Ortopedia', preferredDate: '2025-09-20', priority: 'low' as const, contact: '(11) 97777-7777' },
];
const mockProfessionals = [
{ id: '1', name: 'Dr. Carlos Silva', specialty: 'Cardiologia' },
{ id: '2', name: 'Dra. Maria Santos', specialty: 'Dermatologia' },
{ id: '3', name: 'Dr. João Oliveira', specialty: 'Ortopedia' },
];
export default function AgendaPage() {
const [appointments, setAppointments] = useState(mockAppointments);
const [waitingList, setWaitingList] = useState(mockWaitingList);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedAppointment, setSelectedAppointment] = useState<any>(null);
const [activeTab, setActiveTab] = useState<'agenda' | 'espera'>('agenda');
const handleSaveAppointment = (appointment: any) => {
if (appointment.id) {
setAppointments(prev => prev.map(a => a.id === appointment.id ? appointment : a));
} else {
const newAppointment = {
...appointment,
id: Date.now().toString(),
};
setAppointments(prev => [...prev, newAppointment]);
}
};
const handleEditAppointment = (appointment: any) => {
setSelectedAppointment(appointment);
setIsModalOpen(true);
};
const handleAddAppointment = () => {
setSelectedAppointment(null);
setIsModalOpen(true);
};
const handleCloseModal = () => {
setIsModalOpen(false);
setSelectedAppointment(null);
};
const handleNotifyPatient = (patientId: string) => {
console.log(`Notificando paciente ${patientId}`);
};
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h1 className="text-3xl font-bold text-gray-900">Agendamento</h1>
<div className="flex space-x-2">
<button
onClick={() => setActiveTab('agenda')}
className={`px-4 py-2 rounded-md ${
activeTab === 'agenda'
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-700'
}`}
>
Agenda
</button>
<button
onClick={() => setActiveTab('espera')}
className={`px-4 py-2 rounded-md ${
activeTab === 'espera'
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-700'
}`}
>
Lista de Espera
</button>
</div>
</div>
{activeTab === 'agenda' ? (
<AgendaCalendar
professionals={mockProfessionals}
appointments={appointments}
onAddAppointment={handleAddAppointment}
onEditAppointment={handleEditAppointment}
/>
) : (
<ListaEspera
patients={waitingList}
onNotify={handleNotifyPatient}
onAddToWaitlist={() => {/* implementar */}}
/>
)}
<AppointmentModal
isOpen={isModalOpen}
onClose={handleCloseModal}
onSave={handleSaveAppointment}
professionals={mockProfessionals}
appointment={selectedAppointment}
/>
</div>
);
}

View File

@ -12,12 +12,17 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { SidebarTrigger } from "../ui/sidebar"
export function DashboardHeader() {
export function PagesHeader({ title = "", subtitle = "" }: { title?: string, subtitle?: string }) {
return (
<header className="h-16 border-b border-border bg-background px-6 flex items-center justify-between">
<div className="flex items-center space-x-4">
<h1 className="text-lg font-semibold text-foreground">NÚCLEO DE ESPECIALIDADES</h1>
<div className="flex flex-row items-center gap-4">
<SidebarTrigger />
<div className="flex items-start flex-col justify-center py-2">
<h1 className="text-lg font-semibold text-foreground">{title}</h1>
<p className="text-gray-600">{subtitle}</p>
</div>
</div>
<div className="flex items-center space-x-4">

View File

@ -3,11 +3,35 @@
import Link from "next/link"
import { usePathname } from "next/navigation"
import { cn } from "@/lib/utils"
import { Home, Calendar, Users, UserCheck, FileText, BarChart3, Settings, Stethoscope, User } from "lucide-react"
import {
Sidebar as ShadSidebar,
SidebarHeader,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupLabel,
SidebarGroupContent,
SidebarMenu,
SidebarMenuItem,
SidebarMenuButton,
SidebarRail,
} from "@/components/ui/sidebar"
import {
Home,
Calendar,
Users,
UserCheck,
FileText,
BarChart3,
Settings,
Stethoscope,
User,
} from "lucide-react"
const navigation = [
{ name: "Dashboard", href: "/dashboard", icon: Home },
{ name: "Agendamento", href: "/agendamento", icon: Calendar },
{ name: "Calendario", href: "/calendar", icon: Calendar },
{ name: "Pacientes", href: "/dashboard/pacientes", icon: Users },
{ name: "Médicos", href: "/dashboard/doutores", icon: User },
{ name: "Consultas", href: "/dashboard/consultas", icon: UserCheck },
@ -20,36 +44,61 @@ export function Sidebar() {
const pathname = usePathname()
return (
<div className="w-64 bg-sidebar border-r border-sidebar-border">
<div className="p-6">
<Link href="/" className="flex items-center space-x-2 hover:opacity-80 transition-opacity">
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
<ShadSidebar
/* mude para side="right" se preferir */
side="left"
/* isso faz colapsar para ícones */
collapsible="icon"
className="border-r border-sidebar-border"
>
<SidebarHeader>
<Link
href="/"
className="flex items-center gap-2 hover:opacity-80 transition-opacity pt-2"
>
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center shrink-0">
<Stethoscope className="w-4 h-4 text-primary-foreground" />
</div>
<span className="text-lg font-semibold text-sidebar-foreground">SUSConecta</span>
</Link>
</div>
<nav className="px-3 space-y-1">
{navigation.map((item) => {
const isActive = pathname === item.href
return (
<Link
key={item.name}
href={item.href}
className={cn(
"flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors",
isActive
? "bg-sidebar-accent text-sidebar-accent-foreground"
: "text-sidebar-foreground hover:bg-sidebar-accent/50 hover:text-sidebar-accent-foreground",
)}
>
<item.icon className="mr-3 h-4 w-4" />
{item.name}
</Link>
)
})}
</nav>
</div>
{/* este span some no modo ícone */}
<span className="text-lg font-semibold text-sidebar-foreground group-data-[collapsible=icon]:hidden">
SUSConecta
</span>
</Link>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel className="group-data-[collapsible=icon]:hidden">
Menu
</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{navigation.map((item) => {
const isActive = pathname === item.href
return (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton asChild isActive={isActive}>
<Link href={item.href} className="flex items-center">
<item.icon className="mr-3 h-4 w-4 shrink-0" />
{/* o texto esconde quando colapsa */}
<span className="truncate group-data-[collapsible=icon]:hidden">
{item.name}
</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)
})}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>{/* espaço para perfil/logout, se quiser */}</SidebarFooter>
{/* rail clicável/hover que ajuda a reabrir/fechar */}
<SidebarRail />
</ShadSidebar>
)
}

View File

@ -1,12 +1,14 @@
"use client"
"use client";
import { useState } from "react"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { Menu, X } from "lucide-react"
import { useState } from "react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Menu, X } from "lucide-react";
import { usePathname } from "next/navigation";
export function Header() {
const [isMenuOpen, setIsMenuOpen] = useState(false)
const [isMenuOpen, setIsMenuOpen] = useState(false);
const pathname = usePathname();
return (
<header className="bg-background border-b border-border sticky top-0 z-50">
@ -20,14 +22,21 @@ export function Header() {
</Link>
{}
<nav className="hidden md:flex items-center space-x-8">
<nav className="hidden md:flex items-center gap-10">
<Link
href="/"
className="text-foreground hover:text-primary transition-colors border-b-2 border-primary pb-1"
className={`text-foreground hover:text-primary transition-colors border-b-2 border-b-[transparent] ${
pathname === "/" ? "border-b-blue-500" : ""
}`}
>
Início
</Link>
<Link href="/sobre" className="text-muted-foreground hover:text-primary transition-colors">
<Link
href="/sobre"
className={`text-foreground hover:text-primary transition-colors border-b-2 border-b-[transparent] ${
pathname === "/sobre" ? "border-b-blue-500" : ""
}`}
>
Sobre
</Link>
</nav>
@ -54,7 +63,11 @@ export function Header() {
</div>
{}
<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} />}
</button>
</div>
@ -101,5 +114,5 @@ export function Header() {
)}
</div>
</header>
)
);
}

View File

@ -0,0 +1,65 @@
export 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: "2",
patient: "Pedro Alves",
time: "2025-09-10T10:30",
duration: 45,
type: "retorno" as const,
status: "pending" as const,
professional: "2",
notes: "",
},
{
id: "3",
patient: "Mariana Lima",
time: "2025-09-10T14:00",
duration: 60,
type: "exame" as const,
status: "confirmed" as const,
professional: "3",
notes: "",
},
];
export const mockWaitingList = [
{
id: "1",
name: "Ana Costa",
specialty: "Cardiologia",
preferredDate: "2025-09-12",
priority: "high" as const,
contact: "(11) 99999-9999",
},
{
id: "2",
name: "Pedro Alves",
specialty: "Dermatologia",
preferredDate: "2025-09-15",
priority: "medium" as const,
contact: "(11) 98888-8888",
},
{
id: "3",
name: "Mariana Lima",
specialty: "Ortopedia",
preferredDate: "2025-09-20",
priority: "low" as const,
contact: "(11) 97777-7777",
},
];
export const mockProfessionals = [
{ id: "1", name: "Dr. Carlos Silva", specialty: "Cardiologia" },
{ id: "2", name: "Dra. Maria Santos", specialty: "Dermatologia" },
{ id: "3", name: "Dr. João Oliveira", specialty: "Ortopedia" },
];