inital
This commit is contained in:
174
frontend/src/components/dashboard/ActivityFeed.tsx
Normal file
174
frontend/src/components/dashboard/ActivityFeed.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
Avatar,
|
||||
Box,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
LocalFireDepartment,
|
||||
Person,
|
||||
DirectionsCar,
|
||||
Assignment,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface Activity {
|
||||
id: string;
|
||||
type: 'incident' | 'member' | 'vehicle' | 'task';
|
||||
title: string;
|
||||
description: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
// Placeholder activities
|
||||
const placeholderActivities: Activity[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'incident',
|
||||
title: 'Brandeinsatz',
|
||||
description: 'Kleinbrand in der Hauptstraße',
|
||||
timestamp: 'Vor 2 Stunden',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'member',
|
||||
title: 'Neues Mitglied',
|
||||
description: 'Max Mustermann ist der Feuerwehr beigetreten',
|
||||
timestamp: 'Vor 5 Stunden',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'vehicle',
|
||||
title: 'Fahrzeugwartung',
|
||||
description: 'LF 16/12 - Wartung abgeschlossen',
|
||||
timestamp: 'Gestern',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
type: 'task',
|
||||
title: 'Aufgabe zugewiesen',
|
||||
description: 'Neue Aufgabe: Inventur Atemschutzgeräte',
|
||||
timestamp: 'Vor 2 Tagen',
|
||||
},
|
||||
];
|
||||
|
||||
const ActivityFeed: React.FC = () => {
|
||||
const getActivityIcon = (type: Activity['type']) => {
|
||||
switch (type) {
|
||||
case 'incident':
|
||||
return <LocalFireDepartment />;
|
||||
case 'member':
|
||||
return <Person />;
|
||||
case 'vehicle':
|
||||
return <DirectionsCar />;
|
||||
case 'task':
|
||||
return <Assignment />;
|
||||
default:
|
||||
return <LocalFireDepartment />;
|
||||
}
|
||||
};
|
||||
|
||||
const getActivityColor = (type: Activity['type']) => {
|
||||
switch (type) {
|
||||
case 'incident':
|
||||
return 'error.main';
|
||||
case 'member':
|
||||
return 'success.main';
|
||||
case 'vehicle':
|
||||
return 'warning.main';
|
||||
case 'task':
|
||||
return 'info.main';
|
||||
default:
|
||||
return 'primary.main';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Letzte Aktivitäten
|
||||
</Typography>
|
||||
|
||||
<List sx={{ pt: 2 }}>
|
||||
{placeholderActivities.map((activity, index) => (
|
||||
<React.Fragment key={activity.id}>
|
||||
<ListItem
|
||||
alignItems="flex-start"
|
||||
sx={{
|
||||
px: 0,
|
||||
position: 'relative',
|
||||
'&::before':
|
||||
index < placeholderActivities.length - 1
|
||||
? {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
left: 19,
|
||||
top: 56,
|
||||
bottom: -8,
|
||||
width: 2,
|
||||
bgcolor: 'divider',
|
||||
}
|
||||
: {},
|
||||
}}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<Avatar
|
||||
sx={{
|
||||
bgcolor: getActivityColor(activity.type),
|
||||
width: 40,
|
||||
height: 40,
|
||||
}}
|
||||
>
|
||||
{getActivityIcon(activity.type)}
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography variant="subtitle2" component="span">
|
||||
{activity.title}
|
||||
</Typography>
|
||||
}
|
||||
secondary={
|
||||
<Box>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
component="span"
|
||||
sx={{ display: 'block' }}
|
||||
>
|
||||
{activity.description}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
component="span"
|
||||
>
|
||||
{activity.timestamp}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</List>
|
||||
|
||||
{placeholderActivities.length === 0 && (
|
||||
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Keine Aktivitäten vorhanden
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityFeed;
|
||||
21
frontend/src/components/dashboard/BookstackCard.tsx
Normal file
21
frontend/src/components/dashboard/BookstackCard.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
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;
|
||||
46
frontend/src/components/dashboard/DashboardLayout.tsx
Normal file
46
frontend/src/components/dashboard/DashboardLayout.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useState, ReactNode } from 'react';
|
||||
import { Box, Toolbar } from '@mui/material';
|
||||
import Header from '../shared/Header';
|
||||
import Sidebar from '../shared/Sidebar';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import Loading from '../shared/Loading';
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const { isLoading } = useAuth();
|
||||
|
||||
const handleDrawerToggle = () => {
|
||||
setMobileOpen(!mobileOpen);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading message="Lade Dashboard..." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Header onMenuClick={handleDrawerToggle} />
|
||||
<Sidebar mobileOpen={mobileOpen} onMobileClose={() => setMobileOpen(false)} />
|
||||
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
p: 3,
|
||||
width: { sm: `calc(100% - 240px)` },
|
||||
minHeight: '100vh',
|
||||
backgroundColor: 'background.default',
|
||||
}}
|
||||
>
|
||||
<Toolbar />
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardLayout;
|
||||
21
frontend/src/components/dashboard/NextcloudCard.tsx
Normal file
21
frontend/src/components/dashboard/NextcloudCard.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
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;
|
||||
106
frontend/src/components/dashboard/ServiceCard.tsx
Normal file
106
frontend/src/components/dashboard/ServiceCard.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardActionArea,
|
||||
CardContent,
|
||||
Typography,
|
||||
Box,
|
||||
Chip,
|
||||
} from '@mui/material';
|
||||
import { SvgIconComponent } from '@mui/icons-material';
|
||||
|
||||
interface ServiceCardProps {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: SvgIconComponent;
|
||||
status: 'connected' | 'disconnected';
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const ServiceCard: React.FC<ServiceCardProps> = ({
|
||||
title,
|
||||
description,
|
||||
icon: Icon,
|
||||
status,
|
||||
onClick,
|
||||
}) => {
|
||||
const isConnected = status === 'connected';
|
||||
|
||||
return (
|
||||
<Card
|
||||
sx={{
|
||||
height: '100%',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-4px)',
|
||||
boxShadow: 4,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CardActionArea
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'stretch',
|
||||
justifyContent: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ flexGrow: 1, width: '100%' }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
bgcolor: isConnected ? 'success.light' : 'grey.300',
|
||||
borderRadius: '50%',
|
||||
p: 1.5,
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
sx={{
|
||||
fontSize: 32,
|
||||
color: isConnected ? 'success.dark' : 'grey.600',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: '50%',
|
||||
bgcolor: isConnected ? 'success.main' : 'grey.400',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Typography variant="h6" component="div" gutterBottom>
|
||||
{title}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
{description}
|
||||
</Typography>
|
||||
|
||||
<Chip
|
||||
label={isConnected ? 'Verbunden' : 'Noch nicht konfiguriert'}
|
||||
size="small"
|
||||
color={isConnected ? 'success' : 'default'}
|
||||
variant={isConnected ? 'filled' : 'outlined'}
|
||||
/>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServiceCard;
|
||||
74
frontend/src/components/dashboard/StatsCard.tsx
Normal file
74
frontend/src/components/dashboard/StatsCard.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import { Card, CardContent, Typography, Box } from '@mui/material';
|
||||
import { SvgIconComponent } from '@mui/icons-material';
|
||||
|
||||
interface StatsCardProps {
|
||||
title: string;
|
||||
value: string | number;
|
||||
icon: SvgIconComponent;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
const StatsCard: React.FC<StatsCardProps> = ({
|
||||
title,
|
||||
value,
|
||||
icon: Icon,
|
||||
color = 'primary.main',
|
||||
}) => {
|
||||
return (
|
||||
<Card
|
||||
sx={{
|
||||
height: '100%',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
boxShadow: 3,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
gutterBottom
|
||||
sx={{ textTransform: 'uppercase', fontSize: '0.75rem' }}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography variant="h4" component="div" sx={{ fontWeight: 'bold' }}>
|
||||
{value}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
bgcolor: `${color}15`,
|
||||
borderRadius: '50%',
|
||||
width: 56,
|
||||
height: 56,
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
sx={{
|
||||
fontSize: 32,
|
||||
color: color,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatsCard;
|
||||
137
frontend/src/components/dashboard/UserProfile.tsx
Normal file
137
frontend/src/components/dashboard/UserProfile.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
Avatar,
|
||||
Typography,
|
||||
Box,
|
||||
Chip,
|
||||
} from '@mui/material';
|
||||
import { User } from '../../types/auth.types';
|
||||
|
||||
interface UserProfileProps {
|
||||
user: User;
|
||||
}
|
||||
|
||||
const UserProfile: React.FC<UserProfileProps> = ({ user }) => {
|
||||
// Get first letter of name for avatar
|
||||
const getInitials = (name: string): string => {
|
||||
return name.charAt(0).toUpperCase();
|
||||
};
|
||||
|
||||
// Format date (placeholder until we have actual dates)
|
||||
const formatDate = (date?: string): string => {
|
||||
if (!date) return 'Nicht verfügbar';
|
||||
return new Date(date).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
sx={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
gap: 3,
|
||||
}}
|
||||
>
|
||||
{/* Avatar */}
|
||||
<Avatar
|
||||
sx={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
bgcolor: 'rgba(255, 255, 255, 0.2)',
|
||||
fontSize: '2rem',
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
{getInitials(user.name)}
|
||||
</Avatar>
|
||||
|
||||
{/* User Info */}
|
||||
<Box sx={{ flex: 1, textAlign: { xs: 'center', sm: 'left' } }}>
|
||||
<Typography variant="h5" component="div" gutterBottom>
|
||||
{user.name}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.9 }}>
|
||||
{user.email}
|
||||
</Typography>
|
||||
{user.preferred_username && (
|
||||
<Typography variant="body2" sx={{ opacity: 0.9 }}>
|
||||
@{user.preferred_username}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: 1,
|
||||
mt: 2,
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: { xs: 'center', sm: 'flex-start' },
|
||||
}}
|
||||
>
|
||||
<Chip
|
||||
label="Aktiv"
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: 'rgba(255, 255, 255, 0.2)',
|
||||
color: 'white',
|
||||
}}
|
||||
/>
|
||||
{user.groups && user.groups.length > 0 && (
|
||||
<Chip
|
||||
label={`${user.groups.length} Gruppe${user.groups.length > 1 ? 'n' : ''}`}
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: 'rgba(255, 255, 255, 0.2)',
|
||||
color: 'white',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Additional Info */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 1,
|
||||
textAlign: { xs: 'center', sm: 'right' },
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="caption" sx={{ opacity: 0.8 }}>
|
||||
Letzter Login
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 'medium' }}>
|
||||
Heute
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="caption" sx={{ opacity: 0.8 }}>
|
||||
Mitglied seit
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 'medium' }}>
|
||||
{formatDate()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserProfile;
|
||||
21
frontend/src/components/dashboard/VikunjaCard.tsx
Normal file
21
frontend/src/components/dashboard/VikunjaCard.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
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;
|
||||
8
frontend/src/components/dashboard/index.ts
Normal file
8
frontend/src/components/dashboard/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
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