This commit is contained in:
MDeeApp
2025-10-11 01:17:31 +02:00
commit 8eb060f380
1223 changed files with 265299 additions and 0 deletions

View File

@@ -0,0 +1,147 @@
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);
}