diff --git a/src/App.tsx b/src/App.tsx index b40cef7..abb3d11 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,7 +4,7 @@ import AnimalRegister from "./pages/Animal/AnimalRegister.tsx"; import AnimalList from "./pages/Animal/AnimalList.tsx"; import StaffRegister from "./pages/Staff/StaffRegister.tsx"; import StaffList from "./pages/Staff/StaffList.tsx"; -import Dashboard from "./pages/Dashboard.tsx"; +import Dashboard from "./pages/Dashboard/Dashboard.tsx"; import HealthScreen from "./pages/Health/HealthScreen.tsx"; import AdoptionRegister from "./pages/Adoption/AdoptionRegister.tsx"; import AdoptionList from "./pages/Adoption/AdoptionList.tsx"; diff --git a/src/Components/AlertCard/AlertCard.tsx b/src/Components/AlertCard/AlertCard.tsx new file mode 100644 index 0000000..896af26 --- /dev/null +++ b/src/Components/AlertCard/AlertCard.tsx @@ -0,0 +1,128 @@ +import { motion } from "framer-motion"; +import { AlertTriangle } from "lucide-react"; +import type { Animal } from "../../context/AnimalsContext"; +import type { HealthRecord } from "../../pages/Health/types/healthRecord"; + +interface AlertCardProps { + title: string; + color: "red" | "yellow"; + items: Animal[]; + records: HealthRecord[]; +} + +export default function AlertCard({ title, color, items, records }: AlertCardProps) { + function getDetails(animalId: string) { + const rec = records.filter((r) => r.animalId === animalId); + if (rec.length === 0) return null; + + const sorted = [...rec].sort( + (a, b) => new Date(a.applicationDate).getTime() - new Date(b.applicationDate).getTime() + ); + + const last = sorted[sorted.length - 1]; + const next = last.nextDoseDate ? new Date(last.nextDoseDate) : null; + const today = new Date(); + + const daysLate = + next && next < today + ? Math.floor((today.getTime() - next.getTime()) / (1000 * 60 * 60 * 24)) + : 0; + + return { + lastDose: last.applicationDate + ? new Date(last.applicationDate).toLocaleDateString() + : "—", + nextDose: next ? next.toLocaleDateString() : "—", + daysLate, + }; + } + + const borderColor = { + red: "border-red-500", + yellow: "border-yellow-500", + }; + + const badgeColor = { + red: "bg-red-600", + yellow: "bg-yellow-500", + }; + + return ( + + {/* Header */} +
+ +

{title}

+ + + {items.length} + +
+ + {/* List */} +
+ {items.length === 0 && ( +

Nenhum animal pendente 🎉

+ )} + + {items.map((animal) => { + const details = getDetails(animal.id); + + return ( + + {details && ( +
15 + ? "bg-red-600" + : details.daysLate > 0 + ? "bg-yellow-500" + : "bg-gray-300" + }`} + /> + )} + +
+ {animal.name[0].toUpperCase()} +
+ +
+

{animal.name}

+ + {details && ( +
+

+ Última dose:{" "} + {details.lastDose} +

+

+ Próxima dose:{" "} + {details.nextDose} +

+ + {details.daysLate > 0 && ( +

+ {details.daysLate} dias de atraso +

+ )} +
+ )} +
+ + ); + })} +
+
+ ); +} diff --git a/src/pages/Dashboard/Alerts/HealthAlerts.ts b/src/pages/Dashboard/Alerts/HealthAlerts.ts new file mode 100644 index 0000000..928b7aa --- /dev/null +++ b/src/pages/Dashboard/Alerts/HealthAlerts.ts @@ -0,0 +1,22 @@ +function getAnimalsWithLate(animals: any[], records: any[], field: string) { + const today = new Date(); + + return animals.filter(animal => { + const r = records.filter(x => x.animalId === animal.id); + + if (r.length === 0) return true; + + return r.some(x => + x[field] && new Date(x[field]) < today + ); + }); +} + +export const getAnimalsWithLateVaccines = (animals: any[], records: any[]) => + getAnimalsWithLate(animals, records, "nextDoseDate"); + +export const getAnimalsWithLateAntiparasitics = (animals: any[], records: any[]) => + getAnimalsWithLate(animals, records, "nextApplicationDate"); + +export const getAnimalsWithLateDeworming = (animals: any[], records: any[]) => + getAnimalsWithLate(animals, records, "nextApplicationDate"); diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard/Dashboard.tsx similarity index 53% rename from src/pages/Dashboard.tsx rename to src/pages/Dashboard/Dashboard.tsx index 0dbc25f..a1ac426 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard/Dashboard.tsx @@ -6,7 +6,6 @@ import { FileText, Heart, Calendar, - AlertCircle, } from "lucide-react"; import { LineChart, @@ -19,10 +18,22 @@ import { 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"; +import { getAnimals } from "../../services/animalService.ts"; +import { getStaff } from "../../services/staffService.ts"; +import type { Animal } from "../../context/AnimalsContext.tsx"; +import { useAdoptions } from "../../context/AdoptionsContext.tsx"; +import { getVaccines } from "../../services/vaccineService"; +import { getAntiparasitics } from "../../services/antiparasiticService"; +import { getDewormings } from "../../services/dewormingService"; +import { + getAnimalsWithLateVaccines, + getAnimalsWithLateAntiparasitics, + getAnimalsWithLateDeworming +} from "./Alerts/HealthAlerts.ts"; +import type { HealthRecord } from "../Health/types/healthRecord.ts"; +import AlertCard from "../../Components/AlertCard/AlertCard.tsx"; +import toast from "react-hot-toast"; + export interface Staff { id: string; @@ -38,17 +49,34 @@ export default function Dashboard() { const [animals, setAnimals] = useState([]); const [staff, setStaff] = useState([]); const [loading, setLoading] = useState(true); + const [vaccines, setVaccines] = useState([]); + const [antiparasitics, setAntiparasitics] = useState([]); + const [dewormings, setDewormings] = useState([]); useEffect(() => { async function fetchData() { setLoading(true); try { - const [animalsData, staffData] = await Promise.all([ + const [ + animalsData, + staffData, + vaccinesData, + antiparasiticsData, + dewormingsData + ] = await Promise.all([ getAnimals(), getStaff(), + getVaccines(), + getAntiparasitics(), + getDewormings() ]); + setAnimals(animalsData); setStaff(staffData); + setVaccines(vaccinesData); + setAntiparasitics(antiparasiticsData); + setDewormings(dewormingsData); + } catch (err) { console.error(err); } finally { @@ -61,18 +89,46 @@ export default function Dashboard() { const currentMonth = new Date().getMonth(); const currentYear = new Date().getFullYear(); + const lateVaccines = getAnimalsWithLateVaccines(animals, vaccines); + const lateAntiparasitics = getAnimalsWithLateAntiparasitics(animals, antiparasitics); + const lateDewormings = getAnimalsWithLateDeworming(animals, dewormings); - function parseDate(input: any): Date { - if (!input) return new Date(); - - if (input instanceof Date) return input; + useEffect(() => { + if (!loading) { + const totalAlerts = + lateVaccines.length + + lateAntiparasitics.length + + lateDewormings.length; - if (typeof input === "object") - return input.toDate(); + if (totalAlerts > 0) { + toast.error(`⚠ Existem ${totalAlerts} alertas pendentes de saúde!`, { + duration: 4500, + style: { + background: "#1f1f1f", + color: "#fff", + fontWeight: "600", + padding: "14px 18px", + borderRadius: "12px", + border: "1px solid rgba(239, 68, 68, 0.25)", + boxShadow: "0 4px 14px rgba(0,0,0,0.25)", + fontSize: "14px", + }, + iconTheme: { + primary: "#ef4444", + secondary: "#ffffff", + }, + }); + } + } + }, [loading, lateVaccines, lateAntiparasitics, lateDewormings]); + function parseDate(input: Date | string | number | { toDate?: () => Date } | null | undefined): Date { + if (!input) return new Date(); + if (input instanceof Date) return input; + if (typeof input === "number") return new Date(input); if (typeof input === "string") return new Date(input); - - return new Date(input); + if (typeof input === "object" && typeof input.toDate === "function") return input.toDate(); + return new Date(); } const cadastrosDoMes = animals.filter(animal => { @@ -122,25 +178,6 @@ export default function Dashboard() { }, ]; - 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", - }, - { - id: 2, - message: `${animals.filter((a) => a.needsCheckup).length} animais precisam de checkup`, - color: "yellow", - }, - ]; - const months = [ "Jan", "Fev", @@ -192,30 +229,37 @@ export default function Dashboard() {
- {stats.map((stat) => { - const Icon = stat.icon; - return ( - -
-
- -
-
- - {loading ? "..." : stat.value} - - {stat.label} + {loading ? ( + Array.from({ length: 5 }).map((_, i) => ( +
+ )) + ) : ( + stats.map((stat) => { + const Icon = stat.icon; + return ( + +
+
+ +
+
+ {stat.value} + {stat.label} +
-
- - ); - })} + + ); + }) + )}
@@ -267,7 +311,6 @@ export default function Dashboard() {
- {/* Tabela */}

Últimos Animais Cadastrados @@ -294,47 +337,63 @@ export default function Dashboard() { - {animals.slice(-5).map((animal) => ( - - - {animal.name} - - {animal.breed} - {animal.status} - - {animal.needsVaccine ? "Pendente" : "Ok"} - - - {animal.needsCheckup ? "Pendente" : "Ok"} - - - ))} + {loading + ? Array.from({ length: 5 }).map((_, i) => ( + + +
+ + +
+ + +
+ + +
+ + +
+ + + )) + : animals.slice(-5).map((animal) => ( + + + {animal.name} + + {animal.breed} + {animal.status} + + {animal.needsVaccine ? "Pendente" : "Ok"} + + + {animal.needsCheckup ? "Pendente" : "Ok"} + + + ))}

- {/* Alertas */}

Alertas

- {alerts.map((alert, i) => ( - - - {alert.message} - - ))} + {loading ? ( + Array.from({ length: 3 }).map((_, i) => ( +
+ )) + ) : ( + <> + + + + + )}
diff --git a/src/pages/Health/HealthCardList/HealthCardList.tsx b/src/pages/Health/HealthCardList/HealthCardList.tsx index 9aa111a..54f1615 100644 --- a/src/pages/Health/HealthCardList/HealthCardList.tsx +++ b/src/pages/Health/HealthCardList/HealthCardList.tsx @@ -1,5 +1,3 @@ -// typescript -// src/pages/Health/HealthCardList/HealthCardList.tsx import { useEffect, useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Syringe, Shield, Pill, X, Trash2 } from "lucide-react";