feat: add page of patients of consults day

This commit is contained in:
M-Gabrielly 2025-10-31 01:55:54 -03:00
parent d2e6d8948e
commit 135c416758
4 changed files with 115 additions and 150 deletions

View File

@ -1,138 +0,0 @@
# 3D Wall Calendar Component
## 📦 Componente Integrado
Um calendário interativo 3D com efeitos de parede para visualização de eventos.
## 🎯 Localização
- **Componente**: `components/ui/three-dwall-calendar.tsx`
- **Página**: `app/(main-routes)/calendar/page.tsx`
## 🚀 Funcionalidades
- ✅ Visualização 3D interativa com efeito de perspectiva
- ✅ Controle de rotação via mouse (drag) e scroll
- ✅ Navegação entre meses
- ✅ Adição e remoção de eventos
- ✅ Visualização de eventos por dia
- ✅ Popover com detalhes do evento
- ✅ Hover card para preview rápido
- ✅ Suporte a localização pt-BR
- ✅ Design responsivo
## 🎮 Como Usar
### Na Página de Calendário
Acesse a página de calendário e clique no botão **"3D"** ou pressione a tecla **"3"** para alternar para a visualização 3D.
### Atalhos de Teclado
- **C**: Calendário tradicional (FullCalendar)
- **3**: Calendário 3D
- **F**: Fila de espera
### Interação 3D
- **Arrastar (drag)**: Rotaciona o calendário
- **Scroll do mouse**: Ajusta a inclinação vertical/horizontal
- **Clique nos eventos**: Abre detalhes com opção de remover
## 📝 API do Componente
```tsx
interface CalendarEvent {
id: string
title: string
date: string // ISO format
}
interface ThreeDWallCalendarProps {
events: CalendarEvent[]
onAddEvent?: (e: CalendarEvent) => void
onRemoveEvent?: (id: string) => void
panelWidth?: number // default: 160
panelHeight?: number // default: 120
columns?: number // default: 7
}
```
## 🔧 Dependências Instaladas
- `uuid` - Geração de IDs únicos
- `date-fns` - Manipulação de datas
- `@radix-ui/react-popover` - Popovers
- `@radix-ui/react-hover-card` - Hover cards
- `lucide-react` - Ícones
## 🎨 Personalização
O componente utiliza as variáveis CSS do tema shadcn/ui:
- `bg-blue-500` / `dark:bg-blue-600` para eventos
- Componentes shadcn/ui: `Card`, `Button`, `Input`, `Popover`, `HoverCard`
## 📱 Responsividade
O calendário ajusta automaticamente:
- 7 colunas para desktop (padrão)
- Scroll horizontal para telas menores
- Cards responsivos com overflow visível
## 🔄 Integração com Backend
Os eventos são convertidos automaticamente dos agendamentos do sistema:
```tsx
// Conversão automática de agendamentos para eventos 3D
const threeDEvents: CalendarEvent[] = appointments.map((obj: any) => ({
id: obj.id || String(Date.now()),
title: `${patient}: ${appointment_type}`,
date: new Date(scheduled_at).toISOString(),
}))
```
## ✨ Exemplo de Uso
```tsx
import { ThreeDWallCalendar, CalendarEvent } from "@/components/ui/three-dwall-calendar"
export default function MyPage() {
const [events, setEvents] = useState<CalendarEvent[]>([])
const handleAddEvent = (event: CalendarEvent) => {
setEvents((prev) => [...prev, event])
}
const handleRemoveEvent = (id: string) => {
setEvents((prev) => prev.filter((e) => e.id !== id))
}
return (
<ThreeDWallCalendar
events={events}
onAddEvent={handleAddEvent}
onRemoveEvent={handleRemoveEvent}
/>
)
}
```
## 🐛 Troubleshooting
### Eventos não aparecem
- Verifique se o formato da data está em ISO (`new Date().toISOString()`)
- Confirme que o array `events` está sendo passado corretamente
### Rotação não funciona
- Certifique-se de que o navegador suporta `transform-style: preserve-3d`
- Verifique se não há conflitos de CSS sobrescrevendo as propriedades 3D
### Performance
- Limite o número de eventos por dia para melhor performance
- Considere virtualização para calendários com muitos meses
---
**Data de Integração**: 30 de outubro de 2025
**Versão**: 1.0.0

View File

@ -51,6 +51,7 @@ export function ThreeDWallCalendar({
const isDragging = React.useRef(false)
const dragStart = React.useRef<{ x: number; y: number } | null>(null)
const hasDragged = React.useRef(false)
const clickStart = React.useRef<{ x: number; y: number } | null>(null)
// month days
const days = eachDayOfInterval({
@ -64,11 +65,9 @@ export function ThreeDWallCalendar({
const selectedDayEvents = selectedDay ? eventsForDay(selectedDay) : []
const handleDayClick = (day: Date) => {
// Só abre o dialog se não foi um drag
if (!hasDragged.current) {
setSelectedDay(day)
setIsDialogOpen(true)
}
console.log('Day clicked:', format(day, 'dd/MM/yyyy'))
setSelectedDay(day)
setIsDialogOpen(true)
}
// Add event handler
@ -115,6 +114,10 @@ export function ThreeDWallCalendar({
const onPointerUp = () => {
isDragging.current = false
dragStart.current = null
// Reset hasDragged após um curto delay para permitir o clique ser processado
setTimeout(() => {
hasDragged.current = false
}, 100)
}
const gap = 12
@ -132,6 +135,16 @@ export function ThreeDWallCalendar({
<Button onClick={() => setDateRef((d) => new Date(d.getFullYear(), d.getMonth() + 1, 1))}>
Próximo Mês
</Button>
{/* Botão Pacientes de hoje */}
<Button
variant="outline"
onClick={() => {
setSelectedDay(new Date())
setIsDialogOpen(true)
}}
>
Pacientes de hoje
</Button>
</div>
{/* Legenda de cores */}
@ -168,12 +181,12 @@ export function ThreeDWallCalendar({
onPointerUp={onPointerUp}
onPointerCancel={onPointerUp}
className="w-full overflow-auto"
style={{ perspective: 1200 }}
style={{ perspective: 1200, maxWidth: 1100 }}
>
<div
className="mx-auto"
style={{
width: columns * (panelWidth + gap),
width: Math.max(700, columns * (panelWidth + gap)),
transformStyle: "preserve-3d",
transform: `rotateX(${tiltX}deg) rotateY(${tiltY}deg)`,
transition: "transform 120ms linear",
@ -204,7 +217,21 @@ export function ThreeDWallCalendar({
transform: `translateZ(${z}px)`,
zIndex: Math.round(100 - Math.abs(rowOffset)),
}}
onClick={() => handleDayClick(day)}
onPointerDown={(e) => {
clickStart.current = { x: e.clientX, y: e.clientY }
}}
onPointerUp={(e) => {
if (clickStart.current) {
const dx = Math.abs(e.clientX - clickStart.current.x)
const dy = Math.abs(e.clientY - clickStart.current.y)
// Se moveu menos de 5 pixels, é um clique
if (dx < 5 && dy < 5) {
e.stopPropagation()
handleDayClick(day)
}
clickStart.current = null
}
}}
>
<Card className="h-full overflow-visible hover:shadow-lg transition-shadow">
<CardContent className="p-2 h-full flex flex-col">
@ -318,14 +345,32 @@ export function ThreeDWallCalendar({
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-xl">
{selectedDay && format(selectedDay, "dd 'de' MMMM 'de' yyyy", { locale: ptBR })}
</DialogTitle>
{/* Navegação de dias */}
<div className="flex items-center justify-between mb-2">
<Button
variant="ghost"
size="icon"
onClick={() => setSelectedDay((prev) => prev ? new Date(prev.getFullYear(), prev.getMonth(), prev.getDate() - 1) : new Date())}
aria-label="Dia anterior"
>
&#x276E;
</Button>
<DialogTitle className="text-xl">
{selectedDay && format(selectedDay, "dd 'de' MMMM 'de' yyyy", { locale: ptBR })}
</DialogTitle>
<Button
variant="ghost"
size="icon"
onClick={() => setSelectedDay((prev) => prev ? new Date(prev.getFullYear(), prev.getMonth(), prev.getDate() + 1) : new Date())}
aria-label="Próximo dia"
>
&#x276F;
</Button>
</div>
<DialogDescription>
{selectedDayEvents.length} {selectedDayEvents.length === 1 ? 'paciente agendado' : 'pacientes agendados'}
</DialogDescription>
</DialogHeader>
<div className="space-y-3 mt-4">
{selectedDayEvents.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">

View File

@ -49,6 +49,7 @@
"cmdk": "latest",
"date-fns": "4.1.0",
"embla-carousel-react": "latest",
"framer-motion": "^12.23.24",
"geist": "^1.3.1",
"input-otp": "latest",
"jspdf": "^3.0.3",
@ -65,6 +66,7 @@
"sonner": "latest",
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7",
"uuid": "^13.0.0",
"vaul": "latest",
"zod": "3.25.67"
},
@ -5738,6 +5740,33 @@
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/framer-motion": {
"version": "12.23.24",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz",
"integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.23.23",
"motion-utils": "^12.23.6",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@ -7140,6 +7169,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/motion-dom": {
"version": "12.23.23",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
"integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.23.6"
}
},
"node_modules/motion-utils": {
"version": "12.23.6",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -9175,6 +9219,19 @@
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/uuid": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist-node/bin/uuid"
}
},
"node_modules/vaul": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz",

View File

@ -51,6 +51,7 @@
"cmdk": "latest",
"date-fns": "4.1.0",
"embla-carousel-react": "latest",
"framer-motion": "^12.23.24",
"geist": "^1.3.1",
"input-otp": "latest",
"jspdf": "^3.0.3",