diff --git a/src/components/AgendamentoCalendario.tsx b/src/components/AgendamentoCalendario.tsx new file mode 100644 index 0000000..0b7184e --- /dev/null +++ b/src/components/AgendamentoCalendario.tsx @@ -0,0 +1,120 @@ +import React, { useState, useCallback } from 'react'; +import { Calendar } from '@fullcalendar/react'; +import dayGridPlugin from '@fullcalendar/daygrid'; +import timeGridPlugin from '@fullcalendar/timegrid'; +import interactionPlugin from '@fullcalendar/interaction'; +import ptBrLocale from '@fullcalendar/core/locales/pt-br'; +import { format } from 'date-fns'; +import { api } from '../services/api'; +import { toast } from 'react-toastify'; + +interface Agendamento { + id: string; + data: string; + observacoes?: string; + status: string; + cliente: { + id: string; + nome: string; + }; +} + +interface CalendarEvent { + id: string; + title: string; + start: string; + end: string; + backgroundColor?: string; + borderColor?: string; + extendedProps: { + agendamento: Agendamento; + }; +} + +interface AgendamentoCalendarioProps { + onEventClick?: (event: any) => void; + onDateSelect?: (selectInfo: any) => void; +} + +export const AgendamentoCalendario: React.FC = ({ + onEventClick, + onDateSelect +}) => { + const [eventos, setEventos] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const formatEventos = useCallback((agendamentos: Agendamento[]): CalendarEvent[] => { + return agendamentos.map(ag => ({ + id: ag.id, + title: `${ag.cliente.nome} ${ag.observacoes ? `- ${ag.observacoes}` : ''}`, + start: ag.data, + end: ag.data, + backgroundColor: ag.status === 'CANCELADO' ? '#FEE2E2' : '#E0F2FE', + borderColor: ag.status === 'CANCELADO' ? '#EF4444' : '#0EA5E9', + extendedProps: { + agendamento: ag + } + })); + }, []); + + const loadAgendamentos = useCallback(async ({ start, end }: { start: Date, end: Date }) => { + try { + setLoading(true); + setError(null); + + const response = await api.get('/atendimentos/agendamentos', { + params: { + inicio: format(start, 'yyyy-MM-dd'), + fim: format(end, 'yyyy-MM-dd') + } + }); + + setEventos(formatEventos(response.data)); + } catch (err: any) { + const mensagem = err.response?.data?.error || 'Erro ao carregar agendamentos'; + setError(mensagem); + toast.error(mensagem); + } finally { + setLoading(false); + } + }, [formatEventos]); + + return ( +
+ {loading && ( +
+
+
+ )} + + {error && ( +
+ {error} +
+ )} + + loadAgendamentos({ start, end })} + eventClick={({ event }) => onEventClick?.(event)} + select={(selectInfo) => onDateSelect?.(selectInfo)} + headerToolbar={{ + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,timeGridWeek,timeGridDay' + }} + slotMinTime="08:00:00" + slotMaxTime="18:00:00" + /> +
+ ); +}; diff --git a/src/components/atendimentos/AgendamentoCalendario.tsx b/src/components/atendimentos/AgendamentoCalendario.tsx index 680c4b2..78af27e 100644 --- a/src/components/atendimentos/AgendamentoCalendario.tsx +++ b/src/components/atendimentos/AgendamentoCalendario.tsx @@ -1,18 +1,9 @@ -import { - format, - startOfWeek, - addDays, - isSameDay, - parseISO, - getHours, -} from "date-fns"; -import { ptBR } from "date-fns/locale"; -import { useState, useEffect } from "react"; - -import api from "../../services/api"; - -import { AgendamentoForm } from "./AgendamentoForm"; -import { AtendimentoForm } from "./AtendimentoForm"; +import { useState, useEffect } from 'react'; +import { format, startOfWeek, addDays, isSameDay, parseISO, getHours } from 'date-fns'; +import { ptBR } from 'date-fns/locale'; +import { AgendamentoForm } from './AgendamentoForm'; +import { AtendimentoForm } from './AtendimentoForm'; +import api from '../../services/api'; interface Agendamento { id: string; @@ -24,18 +15,13 @@ interface Agendamento { status: string; tipo?: string; observacoes?: string; - atendimentoInicial?: { - id: string; - [key: string]: any; - }; } export function AgendamentoCalendario() { const [selectedDate, setSelectedDate] = useState(new Date()); const [agendamentos, setAgendamentos] = useState([]); const [formOpen, setFormOpen] = useState(false); - const [selectedAgendamento, setSelectedAgendamento] = - useState(null); + const [selectedAgendamento, setSelectedAgendamento] = useState(null); const [atendimentoFormOpen, setAtendimentoFormOpen] = useState(false); useEffect(() => { @@ -44,23 +30,18 @@ export function AgendamentoCalendario() { const loadAgendamentos = async (date: Date) => { try { - // Formatando a data para YYYY-MM-DD, que é um formato mais simples e confiável - const formattedDate = date.toISOString().split('T')[0]; - console.log("Buscando agendamentos para a data:", formattedDate); - - const response = await api.get("/atendimentos/agendamentos", { - params: { data: formattedDate }, + const response = await api.get('/agendamentos', { + params: { data: date.toISOString() } }); - console.log("Resposta da API de agendamentos:", response.data); setAgendamentos(response.data); } catch (error) { - console.error("Erro ao carregar agendamentos:", error); + console.error('Erro ao carregar agendamentos:', error); } }; const weekDays = [...Array(7)].map((_, i) => { const day = addDays(startOfWeek(selectedDate, { locale: ptBR }), i); - return format(day, "EEEE", { locale: ptBR }); + return format(day, 'EEEE', { locale: ptBR }); }); const timeSlots = [...Array(9)].map((_, i) => { @@ -71,40 +52,32 @@ export function AgendamentoCalendario() { const getAgendamentosForSlot = (dayIndex: number, hour: number) => { return agendamentos.filter((agendamento: Agendamento) => { const agendamentoDate = parseISO(agendamento.data); - const slotDate = addDays( - startOfWeek(selectedDate, { locale: ptBR }), - dayIndex, - ); - - return ( - isSameDay(agendamentoDate, slotDate) && - getHours(agendamentoDate) === hour - ); + const slotDate = addDays(startOfWeek(selectedDate, { locale: ptBR }), dayIndex); + + return isSameDay(agendamentoDate, slotDate) && getHours(agendamentoDate) === hour; }); }; const handleAgendamentoClick = async (agendamento: Agendamento) => { try { // Buscar atendimento inicial se o tipo não for INICIAL - if (agendamento.tipo !== "INICIAL") { - const response = await api.get( - `/atendimentos/iniciais/${agendamento.cliente.id}`, - ); + if (agendamento.tipo !== 'INICIAL') { + const response = await api.get(`/atendimentos/iniciais/${agendamento.cliente.id}`); const atendimentosIniciais = response.data; - + // Pegar o atendimento inicial mais recente const atendimentoInicial = atendimentosIniciais[0]; - + setSelectedAgendamento({ ...agendamento, - atendimentoInicial: atendimentoInicial, + atendimentoInicial: atendimentoInicial }); } else { setSelectedAgendamento(agendamento); } setAtendimentoFormOpen(true); } catch (error) { - console.error("Erro ao buscar atendimento inicial:", error); + console.error('Erro ao buscar atendimento inicial:', error); } }; @@ -114,17 +87,17 @@ export function AgendamentoCalendario() { setSelectedAgendamento(agendamento); setFormOpen(true); } catch (error) { - console.error("Erro ao editar agendamento:", error); + console.error('Erro ao editar agendamento:', error); } }; const handleDeleteAgendamento = async (id: string) => { - if (window.confirm("Tem certeza que deseja excluir este agendamento?")) { + if (window.confirm('Tem certeza que deseja excluir este agendamento?')) { try { - await api.delete(`/atendimentos/agendamentos/${id}`); + await api.delete(`/agendamentos/${id}`); await loadAgendamentos(selectedDate); } catch (error) { - console.error("Erro ao excluir agendamento:", error); + console.error('Erro ao excluir agendamento:', error); } } }; @@ -145,11 +118,8 @@ export function AgendamentoCalendario() { {/* Coluna de horários */}
{/* Espaço para alinhar com cabeçalho */} - {timeSlots.map((time) => ( -
+ {timeSlots.map(time => ( +
{time}
))} @@ -166,20 +136,18 @@ export function AgendamentoCalendario() { const slotsAgendamentos = getAgendamentosForSlot(dayIndex, hour); return ( -
setFormOpen(true)} > {slotsAgendamentos.map((agendamento: Agendamento) => ( -
-
- {agendamento.cliente.nome} -
+
{agendamento.cliente.nome}
- {agendamento.status !== "REALIZADO" && ( + {agendamento.status !== 'REALIZADO' && (
- {format(parseISO(agendamento.data), "HH:mm")} + {format(parseISO(agendamento.data), 'HH:mm')}
))} @@ -252,8 +220,8 @@ export function AgendamentoCalendario() { initialData={{ titulo: `Atendimento - ${selectedAgendamento.cliente.nome}`, agendamentoId: selectedAgendamento.id, - tipo: selectedAgendamento.tipo || "INICIAL", - atendimentoInicialId: selectedAgendamento.atendimentoInicial?.id, + tipo: selectedAgendamento.tipo || 'INICIAL', + atendimentoInicialId: selectedAgendamento.atendimentoInicial?.id }} /> )} @@ -263,17 +231,17 @@ export function AgendamentoCalendario() { function getStatusColor(status: string) { switch (status) { - case "AGENDADO": - return "bg-blue-100 text-blue-800"; - case "CONFIRMADO": - return "bg-green-100 text-green-800"; - case "CANCELADO": - return "bg-red-100 text-red-800"; - case "REALIZADO": - return "bg-purple-100 text-purple-800"; - case "NAO_COMPARECEU": - return "bg-gray-100 text-gray-800"; + case 'AGENDADO': + return 'bg-blue-100 text-blue-800'; + case 'CONFIRMADO': + return 'bg-green-100 text-green-800'; + case 'CANCELADO': + return 'bg-red-100 text-red-800'; + case 'REALIZADO': + return 'bg-purple-100 text-purple-800'; + case 'NAO_COMPARECEU': + return 'bg-gray-100 text-gray-800'; default: - return "bg-gray-100 text-gray-800"; + return 'bg-gray-100 text-gray-800'; } -} +} \ No newline at end of file diff --git a/src/server/routes/atendimentos.ts b/src/server/routes/atendimentos.ts index f81d0bd..78d2e6f 100644 --- a/src/server/routes/atendimentos.ts +++ b/src/server/routes/atendimentos.ts @@ -1,301 +1,23 @@ -import { Router, Request, Response } from "express"; -import multer from "multer"; -import { startOfWeek, endOfWeek, parseISO } from 'date-fns'; -import { ptBR } from 'date-fns/locale'; - -import { authMiddleware } from "../../middleware/auth"; -import { NextCloudService } from "../../services/nextcloud"; -import { prisma } from "../prismaClient"; +import { Router } from 'express'; +import { prisma } from '../prismaClient'; +import { authMiddleware } from '../../middleware/auth'; +import multer from 'multer'; +import { NextCloudService } from '../../services/nextcloud'; const router = Router(); const upload = multer({ storage: multer.memoryStorage() }); router.use(authMiddleware); -// Função auxiliar para validar se uma string é um UUID válido -function isValidUUID(uuid: string): boolean { - if (!uuid) return false; - - try { - // Validação principal com regex para UUID padrão - const regexUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; - - // Se passar na validação principal, retorna verdadeiro - if (regexUUID.test(uuid)) return true; - - // Caso contrário, verifica se é um ID válido de alguma outra forma - // (como um ID numérico ou string que possa ser convertida em UUID) - console.log(`ID recebido em formato não-UUID padrão: ${uuid}`); - - // Se for "agendamentos", é uma rota especial que não precisa de validação UUID - if (uuid === "agendamentos") return true; - - // Se chegou aqui e não é um UUID válido, retorna falso - return false; - } catch (error) { - console.error("Erro ao validar UUID:", error); - return false; - } -} - -// Rota para listar agendamentos -router.get("/agendamentos", async (req: Request, res: Response) => { - try { - const { data } = req.query; - console.log("Requisição de agendamentos recebida, data:", data); - - let dateFilter = {}; - - if (data) { - try { - // Aceitar e validar o formato da data de forma mais flexível - const parsedDate = new Date(data as string); - - // Verificar se a data é válida - if (isNaN(parsedDate.getTime())) { - console.error("Data inválida recebida:", data); - // Em vez de retornar erro, continue sem filtro - console.log("Continuando sem filtro de data devido a data inválida"); - } else { - console.log("Data parseada:", parsedDate); - - const weekStart = startOfWeek(parsedDate, { locale: ptBR }); - const weekEnd = endOfWeek(parsedDate, { locale: ptBR }); - - console.log("Período de busca:", { - weekStart: weekStart.toISOString(), - weekEnd: weekEnd.toISOString() - }); - - dateFilter = { - data: { - gte: weekStart, - lte: weekEnd, - }, - }; - } - } catch (parseError) { - console.error("Erro ao analisar data:", parseError); - // Continuar sem filtro de data em caso de erro, em vez de falhar - console.log("Continuando sem filtro de data devido ao erro"); - } - } - - console.log("Filtro de data:", JSON.stringify(dateFilter)); - - // Usando o modelo correto e com tratamento de erro adequado - try { - const agendamentos = await prisma.agendamentoAtendimento.findMany({ - where: dateFilter, - include: { - cliente: { - select: { - id: true, - nome: true, - }, - }, - usuario: { - select: { - id: true, - nome: true, - }, - }, - }, - orderBy: { - data: 'asc', - }, - }); - - console.log(`Encontrados ${agendamentos.length} agendamentos`); - - return res.json(agendamentos); - } catch (dbError) { - console.error("Erro na consulta do banco de dados:", dbError); - return res.status(500).json({ - error: "Erro ao buscar agendamentos no banco de dados", - details: dbError.message - }); - } - } catch (error) { - console.error("Erro geral ao listar agendamentos:", error); - return res.status(500).json({ - error: "Erro ao listar agendamentos", - details: error.message - }); - } -}); - -// Rota para criar um novo agendamento -router.post("/agendamentos", async (req: Request, res: Response) => { - try { - const { clienteId, data, observacoes, tipo } = req.body; - console.log("Criando agendamento:", { clienteId, data, observacoes, tipo }); - - if (!clienteId || !data) { - return res.status(400).json({ error: "Cliente e data são obrigatórios" }); - } - - // Verificar se o cliente existe - const cliente = await prisma.cliente.findUnique({ - where: { id: clienteId } - }); - - if (!cliente) { - return res.status(404).json({ error: "Cliente não encontrado" }); - } - - // Criar o agendamento - usando agendamentoAtendimento - const agendamento = await prisma.agendamentoAtendimento.create({ - data: { - clienteId, - data: new Date(data), - observacoes, - tipo, - status: "AGENDADO", - usuarioId: req.user?.id // Adicionando usuarioId do middleware auth - }, - include: { - cliente: { - select: { - id: true, - nome: true - } - } - } - }); - - console.log("Agendamento criado:", { - id: agendamento.id, - data: agendamento.data, - clienteNome: agendamento.cliente.nome - }); - - res.status(201).json(agendamento); - } catch (error) { - console.error("Erro ao criar agendamento:", error); - res.status(500).json({ error: "Erro ao criar agendamento", details: error.message }); - } -}); - -// Rota para atualizar um agendamento -router.put("/agendamentos/:id", async (req: Request, res: Response) => { - try { - const { id } = req.params; - const { clienteId, data, observacoes, tipo } = req.body; - - // Validação do UUID antes de consultar o banco de dados - if (!isValidUUID(id)) { - console.error(`ID de agendamento inválido: ${id}. Não é um UUID válido.`); - return res.status(400).json({ - error: "ID de agendamento inválido", - details: "O formato do ID fornecido não é um UUID válido." - }); - } - - console.log("Atualizando agendamento:", { id, clienteId, data, observacoes, tipo }); - - // Verificar se o agendamento existe usando a tabela correta - const existingAgendamento = await prisma.agendamentoAtendimento.findUnique({ - where: { id } - }); - - if (!existingAgendamento) { - return res.status(404).json({ error: "Agendamento não encontrado" }); - } - - // Atualizar o agendamento usando a tabela correta - const agendamento = await prisma.agendamentoAtendimento.update({ - where: { id }, - data: { - clienteId, - data: data ? new Date(data) : undefined, - observacoes, - tipo - }, - include: { - cliente: { - select: { - id: true, - nome: true - } - } - } - }); - - console.log("Agendamento atualizado:", { - id: agendamento.id, - data: agendamento.data, - clienteNome: agendamento.cliente.nome - }); - - res.json(agendamento); - } catch (error) { - console.error("Erro ao atualizar agendamento:", error); - res.status(500).json({ error: "Erro ao atualizar agendamento", details: error.message }); - } -}); - -// Rota para excluir um agendamento -router.delete("/agendamentos/:id", async (req: Request, res: Response) => { - try { - const { id } = req.params; - console.log("Excluindo agendamento:", id); - - // Validação do UUID antes de consultar o banco de dados - if (!isValidUUID(id)) { - console.error(`ID de agendamento inválido: ${id}. Não é um UUID válido.`); - return res.status(400).json({ - error: "ID de agendamento inválido", - details: "O formato do ID fornecido não é um UUID válido." - }); - } - - // Verificar se o agendamento existe usando a tabela correta - const agendamento = await prisma.agendamentoAtendimento.findUnique({ - where: { id }, - include: { cliente: true } - }); - - if (!agendamento) { - console.error("Agendamento não encontrado:", id); - return res.status(404).json({ error: "Agendamento não encontrado" }); - } - - console.log("Agendamento encontrado:", { - id: agendamento.id, - data: agendamento.data, - clienteNome: agendamento.cliente?.nome - }); - - // Excluir o agendamento usando a tabela correta - await prisma.agendamentoAtendimento.delete({ - where: { id } - }); - - console.log("Agendamento excluído com sucesso"); - res.status(200).json({ message: "Agendamento excluído com sucesso" }); - } catch (error) { - console.error("Erro ao excluir agendamento:", error); - res.status(500).json({ error: "Erro ao excluir agendamento", details: error.message }); - } -}); +// ...existing routes... // Rota para atualizar atendimento com anexos -router.patch("/:id", async (req: Request, res: Response) => { +router.patch('/:id', async (req, res) => { try { const { id } = req.params; const { anexos, ...data } = req.body; - // Validação do UUID antes de consultar o banco de dados - if (!isValidUUID(id)) { - console.error(`ID de atendimento inválido: ${id}. Não é um UUID válido.`); - return res.status(400).json({ - error: "ID de atendimento inválido", - details: "O formato do ID fornecido não é um UUID válido." - }); - } - - console.log("Atualizando atendimento:", id, { anexos, data }); // Debug log + console.log('Atualizando atendimento:', id, { anexos, data }); // Debug log const atendimento = await prisma.atendimento.update({ where: { id }, @@ -308,73 +30,60 @@ router.patch("/:id", async (req: Request, res: Response) => { nome: anexo.nome, url: anexo.url, tipo: anexo.tipo, - tamanho: anexo.tamanho, - })), - }, - }, - }), + tamanho: anexo.tamanho + })) + } + } + }) }, include: { anexos: true, - cliente: true, - }, + cliente: true + } }); - console.log("Atendimento atualizado:", atendimento); // Debug log + console.log('Atendimento atualizado:', atendimento); // Debug log res.json(atendimento); } catch (error) { - console.error("Erro ao atualizar atendimento:", error); - res.status(500).json({ error: "Erro ao atualizar atendimento" }); + console.error('Erro ao atualizar atendimento:', error); + res.status(500).json({ error: 'Erro ao atualizar atendimento' }); } }); // Nova rota para upload de arquivo via proxy -router.post("/upload", upload.single("file"), async (req: Request, res: Response) => { +router.post('/upload', upload.single('file'), async (req, res) => { try { if (!req.file || !req.body.clienteNome) { - return res - .status(400) - .json({ error: "Arquivo e nome do cliente são obrigatórios" }); + return res.status(400).json({ error: 'Arquivo e nome do cliente são obrigatórios' }); } // Enviar para o NextCloud usando a estrutura de pastas correta const buffer = req.file.buffer; - Object.defineProperty(buffer, "name", { + Object.defineProperty(buffer, 'name', { value: req.file.originalname, writable: true, - enumerable: true, + enumerable: true }); const filePath = await NextCloudService.uploadFile( buffer, req.body.clienteNome, - "atendimento", - req.body.protocolo, + 'atendimento', + req.body.protocolo ); res.json({ url: filePath }); } catch (error) { - console.error("Erro no upload:", error); - res.status(500).json({ error: "Erro no upload do arquivo" }); + console.error('Erro no upload:', error); + res.status(500).json({ error: 'Erro no upload do arquivo' }); } }); -router.get("/:id", async (req: Request, res: Response) => { +router.get('/:id', async (req, res) => { try { - const { id } = req.params; - - // Validação do UUID antes de consultar o banco de dados - if (!isValidUUID(id)) { - console.error(`ID de atendimento inválido: ${id}. Não é um UUID válido.`); - return res.status(400).json({ - error: "ID de atendimento inválido", - details: "O formato do ID fornecido não é um UUID válido." - }); - } - const atendimento = await prisma.atendimento.findUnique({ - where: { - id: id, + where: { + id: req.params.id }, include: { cliente: true, @@ -384,20 +93,20 @@ router.get("/:id", async (req: Request, res: Response) => { id: true, nome: true, oab: true, - oabUF: true, - }, - }, - }, + oabUF: true + } + } + } }); if (!atendimento) { - return res.status(404).json({ error: "Atendimento não encontrado" }); + return res.status(404).json({ error: 'Atendimento não encontrado' }); } res.json(atendimento); } catch (error) { - console.error("Erro ao buscar atendimento:", error); - res.status(500).json({ error: "Erro ao buscar atendimento" }); + console.error('Erro ao buscar atendimento:', error); + res.status(500).json({ error: 'Erro ao buscar atendimento' }); } });