158 lines
4.1 KiB
TypeScript
158 lines
4.1 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import {
|
|
Box,
|
|
Card,
|
|
CardContent,
|
|
Typography,
|
|
IconButton,
|
|
Alert,
|
|
} from "@mui/material";
|
|
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
|
import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
|
|
import { Range } from "react-date-range";
|
|
|
|
import { DateRangePicker } from "../DateRangePicker/DateRangePicker";
|
|
import { StatItem } from "./components/StatItem";
|
|
import { DEFAULT_DATE_RANGE } from "./constants";
|
|
import { IHealthData } from "@/app/services/health";
|
|
import { getHealthData } from "@/app/services/transactions";
|
|
|
|
import "./GeneralHealthCard.scss";
|
|
|
|
interface IGeneralHealthCardProps {
|
|
initialHealthData?: IHealthData | null;
|
|
initialStats?: Array<{
|
|
label: string;
|
|
value: string | number;
|
|
change: string;
|
|
}> | null;
|
|
initialDateRange?: Range[];
|
|
}
|
|
|
|
const DEBOUNCE_MS = 1000;
|
|
|
|
export const GeneralHealthCard = ({
|
|
initialHealthData = null,
|
|
initialStats = null,
|
|
initialDateRange,
|
|
}: IGeneralHealthCardProps) => {
|
|
const [dateRange, setDateRange] = useState<Range[]>(
|
|
initialDateRange ?? DEFAULT_DATE_RANGE
|
|
);
|
|
const [healthData, setHealthData] = useState<IHealthData | null>(
|
|
initialHealthData
|
|
);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
/**
|
|
* Fetch health data for a given range
|
|
*/
|
|
const fetchHealthData = async (range: Range[]) => {
|
|
const startDate = range[0]?.startDate;
|
|
const endDate = range[0]?.endDate;
|
|
|
|
if (!startDate || !endDate) return;
|
|
|
|
setError(null);
|
|
|
|
try {
|
|
const data = await getHealthData({
|
|
dateStart: startDate.toISOString(),
|
|
dateEnd: endDate.toISOString(),
|
|
});
|
|
|
|
setHealthData(data);
|
|
} catch (err) {
|
|
const message =
|
|
err instanceof Error ? err.message : "Failed to fetch health data";
|
|
setError(message);
|
|
setHealthData(null);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Date picker change handler
|
|
* (state only — side effects are debounced below)
|
|
*/
|
|
const handleDateRangeChange = (newRange: Range[]) => {
|
|
setDateRange(newRange);
|
|
};
|
|
|
|
/**
|
|
* Debounced fetch when date range changes
|
|
*/
|
|
useEffect(() => {
|
|
const currentRange = dateRange[0];
|
|
if (!currentRange?.startDate || !currentRange?.endDate) return;
|
|
|
|
const isInitialDateRange =
|
|
initialDateRange &&
|
|
currentRange.startDate.getTime() ===
|
|
initialDateRange[0]?.startDate?.getTime() &&
|
|
currentRange.endDate.getTime() ===
|
|
initialDateRange[0]?.endDate?.getTime();
|
|
|
|
// Skip fetch if we're still on SSR-provided data
|
|
if (initialHealthData && isInitialDateRange) return;
|
|
|
|
const timeout = setTimeout(() => {
|
|
fetchHealthData(dateRange);
|
|
}, DEBOUNCE_MS);
|
|
|
|
return () => clearTimeout(timeout);
|
|
}, [dateRange]);
|
|
|
|
/**
|
|
* Resolve stats source
|
|
*/
|
|
const stats = healthData?.stats ??
|
|
initialStats ?? [
|
|
{ label: "TOTAL", value: 0, change: "0%" },
|
|
{ label: "SUCCESSFUL", value: 0, change: "0%" },
|
|
{ label: "ACCEPTANCE RATE", value: "0%", change: "0%" },
|
|
{ label: "AMOUNT", value: "€0.00", change: "0%" },
|
|
{ label: "ATV", value: "€0.00", change: "0%" },
|
|
];
|
|
|
|
return (
|
|
<Card className="general-health-card">
|
|
<CardContent>
|
|
<Box className="general-health-card__header">
|
|
<Typography variant="h5" fontWeight="bold">
|
|
General Health
|
|
</Typography>
|
|
|
|
<Box className="general-health-card__right-side">
|
|
<CalendarTodayIcon fontSize="small" />
|
|
<Typography variant="body2">
|
|
<DateRangePicker
|
|
value={dateRange}
|
|
onChange={handleDateRangeChange}
|
|
/>
|
|
</Typography>
|
|
<IconButton size="small">
|
|
<MoreVertIcon fontSize="small" />
|
|
</IconButton>
|
|
</Box>
|
|
</Box>
|
|
|
|
{error && (
|
|
<Alert severity="error" sx={{ mb: 2 }}>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
|
|
{!error && (
|
|
<Box className="general-health-card__stat-items">
|
|
{stats.map((item, i) => (
|
|
<StatItem key={`${item.label}-${i}`} {...item} />
|
|
))}
|
|
</Box>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
};
|