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/server/routes/atendimentos.ts b/src/server/routes/atendimentos.ts index f81d0bd..912fb97 100644 --- a/src/server/routes/atendimentos.ts +++ b/src/server/routes/atendimentos.ts @@ -12,127 +12,129 @@ const upload = multer({ storage: multer.memoryStorage() }); router.use(authMiddleware); +// Rota para listar agendamentos - DEVE VIR ANTES do middleware de validação UUID +router.get("/agendamentos", async (req: Request, res: Response) => { + try { + const { data } = req.query; + console.log("[Agendamentos] Buscando agendamentos para data:", data); + + if (!data) { + return res.status(400).json({ + error: "Data é obrigatória", + details: "Informe uma data válida" + }); + } + + const searchDate = new Date(data as string); + if (isNaN(searchDate.getTime())) { + return res.status(400).json({ + error: "Data inválida", + details: "O formato da data deve ser YYYY-MM-DD" + }); + } + + const agendamentos = await prisma.agendamentoAtendimento.findMany({ + where: { + data: { + gte: startOfWeek(searchDate, { locale: ptBR }), + lte: endOfWeek(searchDate, { locale: ptBR }) + } + }, + include: { + cliente: { + select: { + id: true, + nome: true, + } + }, + usuario: { + select: { + id: true, + nome: true, + } + } + }, + orderBy: { + data: 'asc' + } + }); + + console.log(`[Agendamentos] Encontrados ${agendamentos.length} agendamentos`); + return res.json(agendamentos); + + } catch (error) { + console.error("[Agendamentos] Erro ao buscar:", error); + return res.status(500).json({ + error: "Erro ao buscar agendamentos", + details: error.message + }); + } +}); + +// Middleware para logging com informações mais detalhadas +router.use((req, res, next) => { + const requestId = Math.random().toString(36).substring(7); + console.log(`[${new Date().toISOString()}][${requestId}] Request:`, { + method: req.method, + path: req.path, + originalUrl: req.originalUrl, + params: req.params, + query: req.query, + body: req.body + }); + + // Interceptar resposta para logging + const oldJson = res.json; + res.json = function(data) { + console.log(`[${new Date().toISOString()}][${requestId}] Response:`, { + statusCode: res.statusCode, + data: data + }); + return oldJson.apply(res, arguments); + }; + + next(); +}); + // 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; + return regexUUID.test(uuid); } 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 +// Middleware de validação UUID - DEVE VIR DEPOIS das rotas que não precisam de validação +router.param('id', (req, res, next) => { + const { id } = req.params; + + if (!isValidUUID(id)) { + console.error(`ID inválido: ${id}`); + return res.status(400).json({ + error: "ID inválido", + details: "O formato do ID fornecido não é válido para esta operação" }); } + + next(); }); // 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 }); + const { clienteId, data, observacoes } = req.body; // Removido tipo temporariamente + console.log("[Agendamentos] Criando agendamento:", { clienteId, data, observacoes }); if (!clienteId || !data) { - return res.status(400).json({ error: "Cliente e data são obrigatórios" }); + return res.status(400).json({ + error: "Dados inválidos", + details: "Cliente e data são obrigatórios" + }); } // Verificar se o cliente existe @@ -144,15 +146,14 @@ router.post("/agendamentos", async (req: Request, res: Response) => { return res.status(404).json({ error: "Cliente não encontrado" }); } - // Criar o agendamento - usando agendamentoAtendimento + // Criar o agendamento sem o campo tipo 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 + usuarioId: req.user?.id }, include: { cliente: { @@ -164,16 +165,20 @@ router.post("/agendamentos", async (req: Request, res: Response) => { } }); - console.log("Agendamento criado:", { + console.log("[Agendamentos] Agendamento criado com sucesso:", { id: agendamento.id, data: agendamento.data, clienteNome: agendamento.cliente.nome }); - res.status(201).json(agendamento); + return 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 }); + console.error("[Agendamentos] Erro ao criar agendamento:", error); + return res.status(500).json({ + error: "Erro ao criar agendamento", + details: error.message, + stack: process.env.NODE_ENV === 'development' ? error.stack : undefined + }); } }); @@ -183,15 +188,6 @@ router.put("/agendamentos/:id", async (req: Request, res: Response) => { 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 @@ -241,15 +237,6 @@ router.delete("/agendamentos/:id", async (req: Request, res: Response) => { 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 }, @@ -286,15 +273,6 @@ router.patch("/:id", async (req: Request, res: Response) => { 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 const atendimento = await prisma.atendimento.update({ @@ -363,15 +341,6 @@ router.get("/:id", async (req: Request, res: Response) => { 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,