148 lines
5.2 KiB
TypeScript
148 lines
5.2 KiB
TypeScript
import EventIcon from "@mui/icons-material/Event";
|
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
|
import {
|
|
Accordion,
|
|
AccordionDetails,
|
|
AccordionSummary,
|
|
Chip,
|
|
List,
|
|
ListItem,
|
|
ListItemButton,
|
|
ListItemText,
|
|
Paper,
|
|
Typography
|
|
} from "@mui/material";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { useMemo } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useNavigate } from "react-router-dom";
|
|
|
|
import { fetchUpcomingDeadlines } from "../api/contracts";
|
|
import PageHeader from "../components/PageHeader";
|
|
import { UpcomingDeadline } from "../types";
|
|
import { formatDate } from "../utils/date";
|
|
|
|
const UNKNOWN_MONTH_KEY = "__unknown__";
|
|
|
|
function groupByMonth(deadlines: UpcomingDeadline[], unknownKey: string) {
|
|
const groups = new Map<string, UpcomingDeadline[]>();
|
|
deadlines.forEach((deadline) => {
|
|
const month = deadline.terminationDeadline?.slice(0, 7) ?? unknownKey;
|
|
if (!groups.has(month)) {
|
|
groups.set(month, []);
|
|
}
|
|
groups.get(month)!.push(deadline);
|
|
});
|
|
return Array.from(groups.entries())
|
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
.map(([month, items]) => ({
|
|
month,
|
|
items: items.sort((a, b) => (a.terminationDeadline ?? "").localeCompare(b.terminationDeadline ?? ""))
|
|
}));
|
|
}
|
|
|
|
export default function CalendarView() {
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: ["deadlines", "calendar"],
|
|
queryFn: () => fetchUpcomingDeadlines(365)
|
|
});
|
|
|
|
const groups = useMemo(() => {
|
|
if (!data || !Array.isArray(data)) return [] as ReturnType<typeof groupByMonth>;
|
|
return groupByMonth(data, UNKNOWN_MONTH_KEY);
|
|
}, [data]);
|
|
|
|
const navigate = useNavigate();
|
|
const { t, i18n } = useTranslation();
|
|
|
|
return (
|
|
<>
|
|
<PageHeader
|
|
title={t("calendar.title")}
|
|
subtitle={t("calendar.subtitle")}
|
|
/>
|
|
|
|
<Paper variant="outlined" sx={{ borderRadius: 3 }}>
|
|
{isLoading ? (
|
|
<Typography sx={{ p: 3 }}>{t("calendar.loading")}</Typography>
|
|
) : groups.length === 0 ? (
|
|
<Typography sx={{ p: 3 }} color="text.secondary">
|
|
{t("calendar.none")}
|
|
</Typography>
|
|
) : (
|
|
groups.map(({ month, items }) => (
|
|
<Accordion key={month} defaultExpanded>
|
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
|
<Typography variant="subtitle1" fontWeight={600} display="flex" alignItems="center" gap={1}>
|
|
<EventIcon fontSize="small" />
|
|
{formatMonth(month, i18n.language, t("calendar.unknownMonth"))}
|
|
</Typography>
|
|
<Chip
|
|
label={t("calendar.deadlineCount", { count: items.length })}
|
|
size="small"
|
|
color="primary"
|
|
sx={{ ml: 2 }}
|
|
/>
|
|
</AccordionSummary>
|
|
<AccordionDetails>
|
|
<List>
|
|
{items.map((deadline) => (
|
|
<ListItem
|
|
key={`${month}-${deadline.id}`}
|
|
disablePadding
|
|
secondaryAction={
|
|
deadline.daysUntilDeadline != null ? (
|
|
<Chip
|
|
label={t("deadlineList.daysLabel", { count: deadline.daysUntilDeadline })}
|
|
color={
|
|
deadline.daysUntilDeadline <= 7
|
|
? "error"
|
|
: deadline.daysUntilDeadline <= 21
|
|
? "warning"
|
|
: "default"
|
|
}
|
|
variant="outlined"
|
|
/>
|
|
) : undefined
|
|
}
|
|
>
|
|
<ListItemButton onClick={() => navigate(`/contracts/${deadline.id}`)}>
|
|
<ListItemText
|
|
primary={deadline.title}
|
|
secondary={
|
|
<>
|
|
{t("deadlineList.terminateBy", {
|
|
date: formatDate(deadline.terminationDeadline)
|
|
})}
|
|
{deadline.contractEndDate
|
|
? ` • ${t("deadlineList.contractEnds", {
|
|
date: formatDate(deadline.contractEndDate)
|
|
})}`
|
|
: ""}
|
|
</>
|
|
}
|
|
primaryTypographyProps={{ fontWeight: 600 }}
|
|
/>
|
|
</ListItemButton>
|
|
</ListItem>
|
|
))}
|
|
</List>
|
|
</AccordionDetails>
|
|
</Accordion>
|
|
))
|
|
)}
|
|
</Paper>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function formatMonth(month: string, locale: string, unknownLabel: string): string {
|
|
if (month === UNKNOWN_MONTH_KEY) return unknownLabel;
|
|
const [year, monthNumber] = month.split("-");
|
|
const date = new Date(Number(year), Number(monthNumber) - 1);
|
|
return new Intl.DateTimeFormat(locale.startsWith("de") ? "de-DE" : "en-US", {
|
|
month: "long",
|
|
year: "numeric"
|
|
}).format(date);
|
|
}
|