initial
This commit is contained in:
147
frontend/src/routes/CalendarView.tsx
Normal file
147
frontend/src/routes/CalendarView.tsx
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user