From a0eac143c6e891ca4e8bf726a0ff75e7036dbbfa Mon Sep 17 00:00:00 2001 From: Rafael Gonzaga Date: Mon, 6 Oct 2025 20:30:15 -0300 Subject: [PATCH 1/2] feat: integra o dashboard ao sistema --- src/App.tsx | 39 ++--- src/context/AdoptionsContext.tsx | 48 ++++++ src/pages/Animal/AnimalList.tsx | 44 +++-- src/pages/Animal/AnimalRegister.tsx | 4 +- src/pages/Dashboard.tsx | 241 ++++++++++++++++++++-------- 5 files changed, 282 insertions(+), 94 deletions(-) create mode 100644 src/context/AdoptionsContext.tsx diff --git a/src/App.tsx b/src/App.tsx index 8ed5e94..f3a68bd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,6 +16,7 @@ import {AuthProvider} from "./context/AuthContext"; import {Toaster} from "react-hot-toast"; import type {JSX} from "react"; import {AnimalsProvider} from "./context/AnimalsContext.tsx"; +import { AdoptionsProvider } from "./context/AdoptionsContext"; const ProtectedLayout = ({children}: { children: JSX.Element }) => (
@@ -34,32 +35,34 @@ export default function App() { return ( - + + - - - }/> + + + }/> - }/>}/> - }/>}/> + }/>}/> + }/>}/> - }/>}/> - }/>}/> + }/>}/> + }/>}/> - }/>}/> - }/>}/> + }/>}/> + }/>}/> - }/>}/> - }/>}/> + }/>}/> + }/>}/> - }/>}/> + }/>}/> - }/>}/> - }/>}/> + }/>}/> + }/>}/> - }/>}/> - - + }/>}/> + + + ); diff --git a/src/context/AdoptionsContext.tsx b/src/context/AdoptionsContext.tsx new file mode 100644 index 0000000..d4675b6 --- /dev/null +++ b/src/context/AdoptionsContext.tsx @@ -0,0 +1,48 @@ +import { createContext, useContext, useEffect, useState, type ReactNode } from "react"; +import { collection, onSnapshot } from "firebase/firestore"; +import { db } from "../lib/firebase"; + +export type Adoption = { + id: string; + adopterId: string; + animalId: string; + employeeId: string; + status: string; + notes?: string; + adoptionDate?: string; +}; + +type AdoptionsContextType = { + adoptions: Adoption[]; +}; + +const AdoptionsContext = createContext(undefined); + +export const AdoptionsProvider = ({ children }: { children: ReactNode }) => { + const [adoptions, setAdoptions] = useState([]); + + useEffect(() => { + const adoptionsCol = collection(db, "adoptions"); + const unsubscribe = onSnapshot(adoptionsCol, (snapshot) => { + const formatted = snapshot.docs.map((doc) => ({ + id: doc.id, + ...(doc.data() as Omit), + })); + setAdoptions(formatted); + }); + + return () => unsubscribe(); + }, []); + + return ( + + {children} + + ); +}; + +export const useAdoptions = () => { + const context = useContext(AdoptionsContext); + if (!context) throw new Error("useAdoptions deve ser usado dentro de um AdoptionsProvider"); + return context; +}; diff --git a/src/pages/Animal/AnimalList.tsx b/src/pages/Animal/AnimalList.tsx index 2a2759a..1378a03 100644 --- a/src/pages/Animal/AnimalList.tsx +++ b/src/pages/Animal/AnimalList.tsx @@ -39,12 +39,28 @@ export default function AnimalList() { const calcularIdade = (birthDate?: string) => { if (!birthDate) return "Não informado"; + const nascimento = new Date(birthDate); const hoje = new Date(); - let idade = hoje.getFullYear() - nascimento.getFullYear(); - const m = hoje.getMonth() - nascimento.getMonth(); - if (m < 0 || (m === 0 && hoje.getDate() < nascimento.getDate())) idade--; - return `${idade} ano${idade !== 1 ? "s" : ""}`; + + let anos = hoje.getFullYear() - nascimento.getFullYear(); + let meses = hoje.getMonth() - nascimento.getMonth(); + let dias = hoje.getDate() - nascimento.getDate(); + + if (dias < 0) { + meses--; + const ultimoDiaMesAnterior = new Date(hoje.getFullYear(), hoje.getMonth(), 0).getDate(); + dias += ultimoDiaMesAnterior; + } + + if (meses < 0) { + anos--; + meses += 12; + } + + if (anos > 0) return `${anos} ano${anos !== 1 ? "s" : ""}`; + if (meses > 0) return `${meses} mês${meses !== 1 ? "es" : ""}`; + return `${dias} dia${dias !== 1 ? "s" : ""}`; }; const inputStyle = (error?: boolean) => @@ -161,7 +177,6 @@ export default function AnimalList() {
)} - {/* Modal de Edição */} {editForm && ( - {/* Modal de Exclusão */} {deleteAnimal && ( -
-
+ +

Confirmar exclusão

Deseja realmente excluir {deleteAnimal.name}? @@ -331,8 +355,8 @@ export default function AnimalList() { Excluir

-
- +
+ )} ); diff --git a/src/pages/Animal/AnimalRegister.tsx b/src/pages/Animal/AnimalRegister.tsx index 12f7505..940fa98 100644 --- a/src/pages/Animal/AnimalRegister.tsx +++ b/src/pages/Animal/AnimalRegister.tsx @@ -46,7 +46,9 @@ export default function AnimalRegister() { const validate = () => { const newErrors: { [key: string]: string } = {}; Object.entries(formData).forEach(([key, value]) => { - if (!value) newErrors[key] = "Campo obrigatório"; + if (key !== "notes" && !value) { + newErrors[key] = "Campo obrigatório"; + } }); setErrors(newErrors); return Object.keys(newErrors).length === 0; diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index c64fe16..b797654 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -1,12 +1,28 @@ import { useEffect, useState } from "react"; import { motion } from "framer-motion"; -import { PawPrint, User, FileText, Heart, Calendar, AlertCircle } from "lucide-react"; -import { getAnimals } from "../services/animalService"; -import { getStaff } from "../services/staffService.ts"; import { - LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar, + PawPrint, + User, + FileText, + Heart, + Calendar, + AlertCircle, +} from "lucide-react"; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + BarChart, + Bar, } from "recharts"; +import { getAnimals } from "../services/animalService"; +import { getStaff } from "../services/staffService.ts"; import type { Animal } from "../context/AnimalsContext"; +import { useAdoptions } from "../context/AdoptionsContext"; export interface Staff { id: string; @@ -29,7 +45,7 @@ export default function Dashboard() { try { const [animalsData, staffData] = await Promise.all([ getAnimals(), - getStaff() + getStaff(), ]); setAnimals(animalsData); setStaff(staffData); @@ -43,84 +59,134 @@ export default function Dashboard() { fetchData(); }, []); + const currentMonth = new Date().getMonth(); + const currentYear = new Date().getFullYear(); + + function parseDate(input: any): Date { + if (!input) return new Date(); + + if (input instanceof Date) return input; + + if (typeof input === "object" && input?.toDate instanceof Function) + return input.toDate(); + + if (typeof input === "string") return new Date(input); + + return new Date(input); + } + + const cadastrosDoMes = animals.filter(animal => { + if (!animal.rescueDate) return false; + const date = parseDate(animal.rescueDate); + const month = date.getUTCMonth(); + const year = date.getUTCFullYear(); + return month === currentMonth && year === currentYear; + }).length; + + const stats = [ { id: 1, label: "Total de Animais", value: animals.length, icon: PawPrint, - color: "bg-blue-100 text-blue-800" + color: "bg-blue-100 text-blue-800", }, { id: 2, label: "Funcionários", value: staff.length, icon: User, - color: "bg-green-100 text-green-800" + color: "bg-green-100 text-green-800", }, { id: 3, label: "Adoções", - value: animals.filter(a => a.status === "Adotado").length, + value: animals.filter((a) => a.status === "Adotado").length, icon: FileText, - color: "bg-yellow-100 text-yellow-800" + color: "bg-yellow-100 text-yellow-800", }, { id: 4, - label: "Animais aguardando adoção", - value: animals.filter(a => a.status === "Disponível").length, + label: "Disponíveis para Adoção", + value: animals.filter((a) => a.status === "Disponível").length, icon: Heart, - color: "bg-pink-100 text-pink-800" + color: "bg-pink-100 text-pink-800", }, { id: 5, - label: "Cadastros este mês", - value: 15, + label: "Cadastros do Mês", + value: cadastrosDoMes, icon: Calendar, - color: "bg-purple-100 text-purple-800" + color: "bg-purple-100 text-purple-800", }, ]; + const alertColors = { + red: "border-red-500 bg-red-50 text-red-800", + yellow: "border-yellow-500 bg-yellow-50 text-yellow-800", + green: "border-green-500 bg-green-50 text-green-800", + }; + const alerts = [ { id: 1, - message: `${animals.filter(a => a.needsVaccine).length} animais precisam de vacinação`, - color: "red" + message: `${animals.filter((a) => a.needsVaccine).length} animais precisam de vacinação`, + color: "red", }, { id: 2, - message: `${animals.filter(a => a.needsCheckup).length} animais precisam de checkup`, - color: "yellow" + message: `${animals.filter((a) => a.needsCheckup).length} animais precisam de checkup`, + color: "yellow", }, ]; - const months = ["Jan", "Fev", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Out", "Nov", "Dez"]; + const months = [ + "Jan", + "Fev", + "Mar", + "Abr", + "Mai", + "Jun", + "Jul", + "Ago", + "Set", + "Out", + "Nov", + "Dez", + ]; + + const { adoptions } = useAdoptions(); const adoptionData = months.map((month, index) => { - const count = animals.filter(a => { - if (a.status !== "Adotado" || !a.adoptionDate) return false; + const count = adoptions.filter(a => { + if (!a.adoptionDate) return false; const adoptionMonth = new Date(a.adoptionDate).getMonth(); return adoptionMonth === index; }).length; + return { month, adoptions: count }; }); - const registrationData = [ - { month: "Jan", registrations: 3 }, - { month: "Fev", registrations: 5 }, - { month: "Mar", registrations: 7 }, - { month: "Abr", registrations: 4 }, - { month: "Mai", registrations: 9 }, - { month: "Jun", registrations: 6 }, - ]; + const registrationData = months.map((month, index) => { + const count = animals.filter(animal => { + if (!animal.rescueDate) return false; + const date = parseDate(animal.rescueDate); + const monthUTC = date.getUTCMonth(); + const yearUTC = date.getUTCFullYear(); + return monthUTC === index && yearUTC === currentYear; + }).length; + + return { month, registrations: count }; + }); return ( -
+
Dashboard @@ -131,17 +197,19 @@ export default function Dashboard() { return (
- {loading ? "-" : stat.value} + + {loading ? "..." : stat.value} + {stat.label}
@@ -152,15 +220,22 @@ export default function Dashboard() {
-

Adoções nos últimos meses

+

+ Adoções por Mês +

- {adoptionData.some(d => d.adoptions > 0) ? ( + {adoptionData.some((d) => d.adoptions > 0) ? ( - + ) : (
@@ -171,40 +246,70 @@ export default function Dashboard() {
-

Cadastros nos últimos meses

+

+ Cadastros nos últimos meses +

- - - - - - - + {registrationData.some(d => d.registrations > 0) ? ( + + + + + + + + ) : ( +
+ Nenhum cadastro disponível +
+ )}
+ {/* Tabela */}
-

Últimos Animais Cadastrados

+

+ Últimos Animais Cadastrados +

- - - - - + + + + + {animals.slice(-5).map((animal) => ( - - - - - - + + + + + + ))} @@ -212,17 +317,23 @@ export default function Dashboard() { + {/* Alertas */}

Alertas

- {alerts.map((alert) => ( -
( + {alert.message} -
+ ))}
From 63fb150d47e87603ccd08a42c85f31cbc68914f8 Mon Sep 17 00:00:00 2001 From: Rafael Gonzaga Date: Mon, 6 Oct 2025 20:31:20 -0300 Subject: [PATCH 2/2] feat: integra o dashboard ao sistema --- src/pages/Dashboard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index b797654..64bdf1f 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -67,7 +67,7 @@ export default function Dashboard() { if (input instanceof Date) return input; - if (typeof input === "object" && input?.toDate instanceof Function) + if (typeof input === "object") return input.toDate(); if (typeof input === "string") return new Date(input);
NomeRaçaStatusVacinaCheckup + Nome + + Raça + + Status + + Vacina + + Checkup +
{animal.name}{animal.breed}{animal.status}{animal.needsVaccine ? "Pendente" : "Ok"}{animal.needsCheckup ? "Pendente" : "Ok"}
+ {animal.name} + {animal.breed}{animal.status} + {animal.needsVaccine ? "Pendente" : "Ok"} + + {animal.needsCheckup ? "Pendente" : "Ok"} +