159 lines
4.2 KiB
TypeScript
159 lines
4.2 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import {
|
|
Box,
|
|
Card,
|
|
CardContent,
|
|
Typography,
|
|
IconButton,
|
|
Alert,
|
|
CircularProgress,
|
|
} 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 { useDebouncedDateRange } from "@/app/hooks/useDebouncedDateRange";
|
|
import { dashboardService } from "@/app/services/dashboardService";
|
|
import { normalizeDateRangeForAPI } from "@/app/utils/formatDate";
|
|
|
|
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 [healthData, setHealthData] = useState<IHealthData | null>(
|
|
initialHealthData
|
|
);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
/**
|
|
* Fetch health data for a given range
|
|
*/
|
|
const fetchHealthData = async (range: Range[]) => {
|
|
if (!range || !range[0]) return;
|
|
|
|
const startDate = range[0]?.startDate;
|
|
const endDate = range[0]?.endDate;
|
|
|
|
if (!startDate || !endDate) return;
|
|
|
|
setError(null);
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
// Normalize dates to ensure full day coverage
|
|
const { dateStart, dateEnd } = normalizeDateRangeForAPI(
|
|
startDate,
|
|
endDate
|
|
);
|
|
|
|
// This will update the service and notify all subscribers
|
|
const { healthData } = await dashboardService.fetchDashboardData({
|
|
dateStart,
|
|
dateEnd,
|
|
});
|
|
|
|
setHealthData(healthData);
|
|
} catch (err) {
|
|
const message =
|
|
err instanceof Error ? err.message : "Failed to fetch health data";
|
|
setError(message);
|
|
setHealthData(null);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const { dateRange, handleDateRangeChange } = useDebouncedDateRange({
|
|
initialDateRange: initialDateRange ?? DEFAULT_DATE_RANGE,
|
|
debounceMs: DEBOUNCE_MS,
|
|
onDateRangeChange: fetchHealthData,
|
|
skipInitialFetch: !!initialHealthData,
|
|
});
|
|
|
|
/**
|
|
* 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>
|
|
)}
|
|
|
|
{isLoading ? (
|
|
<Box
|
|
sx={{
|
|
display: "flex",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
minHeight: 100,
|
|
}}
|
|
>
|
|
<CircularProgress />
|
|
</Box>
|
|
) : (
|
|
!error && (
|
|
<Box className="general-health-card__stat-items">
|
|
{stats.map((item, i) => (
|
|
<StatItem key={`${item.label}-${i}`} {...item} />
|
|
))}
|
|
</Box>
|
|
)
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
};
|