featuer change for calendar
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
AlertTitle,
|
||||
Box,
|
||||
CircularProgress,
|
||||
Divider,
|
||||
Link,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { NotificationsActive as NotificationsActiveIcon } from '@mui/icons-material';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { atemschutzApi } from '../../services/atemschutz';
|
||||
import type { User } from '../../types/auth.types';
|
||||
import type { AtemschutzUebersicht } from '../../types/atemschutz.types';
|
||||
|
||||
// ── Constants ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Show a warning banner if a deadline is within this many days. */
|
||||
const WARNING_THRESHOLD_DAYS = 60;
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface PersonalWarning {
|
||||
key: string;
|
||||
/** Negative = overdue, 0 = today, positive = days remaining. */
|
||||
tageRest: number;
|
||||
label: string;
|
||||
/** Short description of what the user should do */
|
||||
action: string;
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function buildWarnings(record: AtemschutzUebersicht): PersonalWarning[] {
|
||||
const warnings: PersonalWarning[] = [];
|
||||
|
||||
if (record.untersuchung_tage_rest \!== null && record.untersuchung_tage_rest <= WARNING_THRESHOLD_DAYS) {
|
||||
warnings.push({
|
||||
key: 'untersuchung',
|
||||
tageRest: record.untersuchung_tage_rest,
|
||||
label: 'Atemschutz-Untersuchung',
|
||||
action: 'Termin beim Betriebsarzt vereinbaren',
|
||||
});
|
||||
}
|
||||
|
||||
if (record.leistungstest_tage_rest \!== null && record.leistungstest_tage_rest <= WARNING_THRESHOLD_DAYS) {
|
||||
warnings.push({
|
||||
key: 'leistungstest',
|
||||
tageRest: record.leistungstest_tage_rest,
|
||||
label: 'Leistungstest',
|
||||
action: 'Atemschutzwart kontaktieren',
|
||||
});
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
function tageText(tage: number): string {
|
||||
if (tage < 0) {
|
||||
const abs = Math.abs(tage);
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from 'react';
|
||||
import { MenuBook } from '@mui/icons-material';
|
||||
import ServiceCard from './ServiceCard';
|
||||
|
||||
interface BookstackCardProps {
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const BookstackCard: React.FC<BookstackCardProps> = ({ onClick }) => {
|
||||
return (
|
||||
<ServiceCard
|
||||
title="Bookstack"
|
||||
description="Dokumentation und Wiki"
|
||||
icon={MenuBook}
|
||||
status="disconnected"
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BookstackCard;
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Cloud } from '@mui/icons-material';
|
||||
import ServiceCard from './ServiceCard';
|
||||
|
||||
interface NextcloudCardProps {
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const NextcloudCard: React.FC<NextcloudCardProps> = ({ onClick }) => {
|
||||
return (
|
||||
<ServiceCard
|
||||
title="Nextcloud"
|
||||
description="Dateien und Dokumente"
|
||||
icon={Cloud}
|
||||
status="disconnected"
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NextcloudCard;
|
||||
@@ -4,9 +4,11 @@ import {
|
||||
AlertTitle,
|
||||
Box,
|
||||
CircularProgress,
|
||||
Divider,
|
||||
Link,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { NotificationsActive as NotificationsActiveIcon } from '@mui/icons-material';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { atemschutzApi } from '../../services/atemschutz';
|
||||
import type { User } from '../../types/auth.types';
|
||||
@@ -24,6 +26,8 @@ interface PersonalWarning {
|
||||
/** Negative = overdue, 0 = today, positive = days remaining. */
|
||||
tageRest: number;
|
||||
label: string;
|
||||
/** Short description of what the user should do */
|
||||
action: string;
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
@@ -31,19 +35,21 @@ interface PersonalWarning {
|
||||
function buildWarnings(record: AtemschutzUebersicht): PersonalWarning[] {
|
||||
const warnings: PersonalWarning[] = [];
|
||||
|
||||
if (record.untersuchung_tage_rest !== null && record.untersuchung_tage_rest <= WARNING_THRESHOLD_DAYS) {
|
||||
if (record.untersuchung_tage_rest \!== null && record.untersuchung_tage_rest <= WARNING_THRESHOLD_DAYS) {
|
||||
warnings.push({
|
||||
key: 'untersuchung',
|
||||
tageRest: record.untersuchung_tage_rest,
|
||||
label: 'Atemschutz-Untersuchung',
|
||||
action: 'Termin beim Betriebsarzt vereinbaren',
|
||||
});
|
||||
}
|
||||
|
||||
if (record.leistungstest_tage_rest !== null && record.leistungstest_tage_rest <= WARNING_THRESHOLD_DAYS) {
|
||||
if (record.leistungstest_tage_rest \!== null && record.leistungstest_tage_rest <= WARNING_THRESHOLD_DAYS) {
|
||||
warnings.push({
|
||||
key: 'leistungstest',
|
||||
tageRest: record.leistungstest_tage_rest,
|
||||
label: 'Leistungstest',
|
||||
action: 'Atemschutzwart kontaktieren',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -63,9 +69,11 @@ function tageText(tage: number): string {
|
||||
|
||||
interface PersonalWarningsBannerProps {
|
||||
user: User;
|
||||
/** Called with the number of active warnings (for badge display) */
|
||||
onWarningCount?: (count: number) => void;
|
||||
}
|
||||
|
||||
const PersonalWarningsBanner: React.FC<PersonalWarningsBannerProps> = ({ user: _user }) => {
|
||||
const PersonalWarningsBanner: React.FC<PersonalWarningsBannerProps> = ({ user: _user, onWarningCount }) => {
|
||||
const [record, setRecord] = useState<AtemschutzUebersicht | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [fetchError, setFetchError] = useState<string | null>(null);
|
||||
@@ -112,13 +120,18 @@ const PersonalWarningsBanner: React.FC<PersonalWarningsBannerProps> = ({ user: _
|
||||
|
||||
// ── No atemschutz record for this user ─────────────────────────────────────
|
||||
|
||||
if (!record) return null;
|
||||
if (\!record) return null;
|
||||
|
||||
// ── Build warnings list ────────────────────────────────────────────────────
|
||||
|
||||
const warnings = buildWarnings(record);
|
||||
|
||||
if (warnings.length === 0) return null;
|
||||
if (warnings.length === 0) {
|
||||
onWarningCount?.(0);
|
||||
return null;
|
||||
}
|
||||
|
||||
onWarningCount?.(warnings.length);
|
||||
|
||||
// ── Render ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -127,56 +140,100 @@ const PersonalWarningsBanner: React.FC<PersonalWarningsBannerProps> = ({ user: _
|
||||
const upcoming = warnings.filter((w) => w.tageRest >= 0);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
{overdue.length > 0 && (
|
||||
<Alert severity="error" variant="outlined">
|
||||
<AlertTitle sx={{ fontWeight: 600 }}>Überfällig — Handlungsbedarf</AlertTitle>
|
||||
<Box component="ul" sx={{ m: 0, pl: 2 }}>
|
||||
{overdue.map((w) => (
|
||||
<Box key={w.key} component="li" sx={{ mb: 0.5 }}>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to="/atemschutz"
|
||||
color="inherit"
|
||||
underline="hover"
|
||||
sx={{ fontWeight: 500 }}
|
||||
>
|
||||
{w.label}
|
||||
</Link>
|
||||
{' — '}
|
||||
<Typography component="span" variant="body2">
|
||||
{tageText(w.tageRest)}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Alert>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
border: '1px solid',
|
||||
borderColor: overdue.length > 0 ? 'error.main' : 'warning.main',
|
||||
borderRadius: 1,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/* Banner header */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
px: 2,
|
||||
py: 1,
|
||||
bgcolor: overdue.length > 0 ? 'error.light' : 'warning.light',
|
||||
}}
|
||||
>
|
||||
<NotificationsActiveIcon
|
||||
fontSize="small"
|
||||
sx={{ color: overdue.length > 0 ? 'error.dark' : 'warning.dark' }}
|
||||
/>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
color: overdue.length > 0 ? 'error.dark' : 'warning.dark',
|
||||
}}
|
||||
>
|
||||
Persönliche Warnungen ({warnings.length})
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{upcoming.length > 0 && (
|
||||
<Alert severity="warning" variant="outlined">
|
||||
<AlertTitle sx={{ fontWeight: 600 }}>Frist läuft bald ab</AlertTitle>
|
||||
<Box component="ul" sx={{ m: 0, pl: 2 }}>
|
||||
{upcoming.map((w) => (
|
||||
<Box key={w.key} component="li" sx={{ mb: 0.5 }}>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to="/atemschutz"
|
||||
color="inherit"
|
||||
underline="hover"
|
||||
sx={{ fontWeight: 500 }}
|
||||
>
|
||||
{w.label}
|
||||
</Link>
|
||||
{' — '}
|
||||
<Typography component="span" variant="body2">
|
||||
{tageText(w.tageRest)}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Alert>
|
||||
)}
|
||||
<Divider />
|
||||
|
||||
{/* Warning alerts */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5, p: 1.5 }}>
|
||||
{overdue.length > 0 && (
|
||||
<Alert severity="error" variant="outlined">
|
||||
<AlertTitle sx={{ fontWeight: 600 }}>Überfällig — Handlungsbedarf</AlertTitle>
|
||||
<Box component="ul" sx={{ m: 0, pl: 2 }}>
|
||||
{overdue.map((w) => (
|
||||
<Box key={w.key} component="li" sx={{ mb: 0.5 }}>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to="/atemschutz"
|
||||
color="inherit"
|
||||
underline="hover"
|
||||
sx={{ fontWeight: 500 }}
|
||||
>
|
||||
{w.label}
|
||||
</Link>
|
||||
{' — '}
|
||||
<Typography component="span" variant="body2">
|
||||
{tageText(w.tageRest)}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.25 }}>
|
||||
→ {w.action}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{upcoming.length > 0 && (
|
||||
<Alert severity="warning" variant="outlined">
|
||||
<AlertTitle sx={{ fontWeight: 600 }}>Frist läuft bald ab</AlertTitle>
|
||||
<Box component="ul" sx={{ m: 0, pl: 2 }}>
|
||||
{upcoming.map((w) => (
|
||||
<Box key={w.key} component="li" sx={{ mb: 0.5 }}>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to="/atemschutz"
|
||||
color="inherit"
|
||||
underline="hover"
|
||||
sx={{ fontWeight: 500 }}
|
||||
>
|
||||
{w.label}
|
||||
</Link>
|
||||
{' — '}
|
||||
<Typography component="span" variant="body2">
|
||||
{tageText(w.tageRest)}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.25 }}>
|
||||
→ {w.action}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Assignment } from '@mui/icons-material';
|
||||
import ServiceCard from './ServiceCard';
|
||||
|
||||
interface VikunjaCardProps {
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const VikunjaCard: React.FC<VikunjaCardProps> = ({ onClick }) => {
|
||||
return (
|
||||
<ServiceCard
|
||||
title="Vikunja"
|
||||
description="Aufgaben und Projekte"
|
||||
icon={Assignment}
|
||||
status="disconnected"
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default VikunjaCard;
|
||||
@@ -1,8 +1,5 @@
|
||||
export { default as UserProfile } from './UserProfile';
|
||||
export { default as ServiceCard } from './ServiceCard';
|
||||
export { default as NextcloudCard } from './NextcloudCard';
|
||||
export { default as VikunjaCard } from './VikunjaCard';
|
||||
export { default as BookstackCard } from './BookstackCard';
|
||||
export { default as StatsCard } from './StatsCard';
|
||||
export { default as ActivityFeed } from './ActivityFeed';
|
||||
export { default as DashboardLayout } from './DashboardLayout';
|
||||
|
||||
Reference in New Issue
Block a user