diff --git a/susconecta/components/ui/THREE_D_CALENDAR_README.md b/susconecta/components/ui/THREE_D_CALENDAR_README.md deleted file mode 100644 index 25f478e..0000000 --- a/susconecta/components/ui/THREE_D_CALENDAR_README.md +++ /dev/null @@ -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([]) - - const handleAddEvent = (event: CalendarEvent) => { - setEvents((prev) => [...prev, event]) - } - - const handleRemoveEvent = (id: string) => { - setEvents((prev) => prev.filter((e) => e.id !== id)) - } - - return ( - - ) -} -``` - -## 🐛 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 diff --git a/susconecta/components/ui/three-dwall-calendar.tsx b/susconecta/components/ui/three-dwall-calendar.tsx index 2d671f8..35c23f3 100644 --- a/susconecta/components/ui/three-dwall-calendar.tsx +++ b/susconecta/components/ui/three-dwall-calendar.tsx @@ -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({ + {/* Botão Pacientes de hoje */} + {/* 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 }} >
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 + } + }} > @@ -318,14 +345,32 @@ export function ThreeDWallCalendar({ - - {selectedDay && format(selectedDay, "dd 'de' MMMM 'de' yyyy", { locale: ptBR })} - + {/* Navegação de dias */} +
+ + + {selectedDay && format(selectedDay, "dd 'de' MMMM 'de' yyyy", { locale: ptBR })} + + +
{selectedDayEvents.length} {selectedDayEvents.length === 1 ? 'paciente agendado' : 'pacientes agendados'}
-
{selectedDayEvents.length === 0 ? (
diff --git a/susconecta/package-lock.json b/susconecta/package-lock.json index ebf1400..7e517ab 100644 --- a/susconecta/package-lock.json +++ b/susconecta/package-lock.json @@ -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", diff --git a/susconecta/package.json b/susconecta/package.json index fe34a22..e7e4ae7 100644 --- a/susconecta/package.json +++ b/susconecta/package.json @@ -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",