feat: add page of patients of consults day
This commit is contained in:
parent
d2e6d8948e
commit
135c416758
@ -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
|
||||
@ -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"
|
||||
>
|
||||
❮
|
||||
</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"
|
||||
>
|
||||
❯
|
||||
</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">
|
||||
|
||||
57
susconecta/package-lock.json
generated
57
susconecta/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user