* @file Dashboard view with informative widgets
*/
-import React, { useContext, useEffect, useState } from 'react';
-import { Container, Grid, Card, CardContent, Typography, Box, Button } from '@mui/material';
-import { MaterialReactTable } from 'material-react-table';
+import React, { useContext, useEffect, useMemo, useState } from 'react';
+import { Box, Button, Card, CardContent, Container, Grid, Typography } from '@mui/material';
+import { MaterialReactTable, useMaterialReactTable } from 'material-react-table';
import { useNavigate } from 'react-router-dom';
import { useSnackbar } from 'notistack';
import { AuthContext } from '../../components/AuthContext';
-import { MessageService, MediaService, PostService, MessageGroupService } from '../../services';
+import { MediaService, MessageGroupService, MessageService, PostService } from '../../services';
import { getFormattedDate, truncateContent } from '../../utils';
+/**
+ * @typedef {Object} User
+ * @property {number} id
+ * @property {string} first_name
+ * @property {string} last_name
+ * @property {string} email
+ */
+
const Dashboard = () => {
const navigate = useNavigate();
const { enqueueSnackbar } = useSnackbar();
- const { user } = useContext(AuthContext);
- const [recentMessages, setRecentMessages] = useState([]);
- const [recentMedia, setRecentMedia] = useState([]);
- const [recentPosts, setRecentPosts] = useState([]);
- const [recentGroups, setRecentGroups] = useState([]);
-
- useEffect(() => {
- const fetchData = async () => {
- try {
- // Fetch recent messages
- const messages = await MessageService.getMessages({ limit: 5 });
- setRecentMessages(messages.data);
-
- // Fetch recent media (filtered by visibility for non-Admin)
- const mediaParams = user?.roles.includes('Admin') ? { limit: 5 } : { limit: 5, visibility: ['public', 'family'] };
- const media = await MediaService.getMedia(mediaParams);
- setRecentMedia(media.data);
-
- // Fetch recent posts (filtered by visibility for non-Admin)
- const postsParams = user?.roles.includes('Admin') ? { limit: 5 } : { limit: 5, visibility: ['public', 'family'] };
- const posts = await PostService.getPosts(postsParams);
- setRecentPosts(posts.data);
-
- // Fetch recent message groups
- const groups = await MessageGroupService.getMessageGroups({ limit: 5 });
- setRecentGroups(groups.data);
- } catch (error) {
- enqueueSnackbar(`Error fetching dashboard data: ${error.message}`, { variant: 'error' });
- }
- };
- fetchData();
- }, [enqueueSnackbar, user]);
+ const { user } = useContext( AuthContext );
+ const [recentMessages, setRecentMessages] = useState( [] );
+ const [recentMedia, setRecentMedia] = useState( [] );
+ const [recentPosts, setRecentPosts] = useState( [] );
+ const [recentGroups, setRecentGroups] = useState( [] );
+
+ const fetchData = useMemo( () => async () => {
+ try {
+ // Fetch recent messages
+ const messages = await MessageService.getMessages( { limit:5 } );
+ setRecentMessages( messages.data );
+
+ // Fetch recent media (filtered by visibility for non-Admin)
+ const mediaParams = user?.roles.includes( 'Admin' ) ? { limit:5 } : { limit:5, visibility:['public', 'family'] };
+ const media = await MediaService.getMedia( mediaParams );
+ setRecentMedia( media.data );
+
+ // Fetch recent posts (filtered by visibility for non-Admin)
+ const postsParams = user?.roles.includes( 'Admin' ) ? { limit:5 } : { limit:5, visibility:['public', 'family'] };
+ const posts = await PostService.getPosts( postsParams );
+ setRecentPosts( posts.data );
+
+ // Fetch recent message groups
+ const groups = await MessageGroupService.getMessageGroups( { limit:5 } );
+ setRecentGroups( groups.data );
+ } catch (error) {
+ enqueueSnackbar( `Error fetching dashboard data: ${ error.message }`, { variant:'error' } );
+ }
+ }, [enqueueSnackbar, user] );
+
+ useEffect( () => {
+ fetchData().catch();
+ }, [enqueueSnackbar, user, fetchData] );
const messageColumns = [
- { accessorKey: 'sender_id', header: 'Sender ID', size: 100 },
- { accessorKey: 'content', header: 'Content', size: 200, Cell: ({ cell }) => cell.getValue() },
- { accessorKey: 'created_at', header: 'Sent At', size: 150, Cell: ({ cell }) => getFormattedDate(cell.getValue()) },
+ { accessorKey:'sender_id', header:'Sender ID', size:100 },
+ { accessorKey:'content', header:'Content', size:200, Cell:( { cell } ) => truncateContent( cell.getValue(), 50 ) },
+ { accessorKey:'read', header:'Read', size:80, Cell:( { cell } ) => (cell.getValue() ? 'Yes' : 'No') },
+ { accessorKey:'created_at', header:'Sent At', size:150, Cell:( { cell } ) => getFormattedDate( cell.getValue() ) },
];
const mediaColumns = [
- { accessorKey: 'file_path', header: 'File Path', size: 200 },
- { accessorKey: 'file_type', header: 'Type', size: 100 },
- { accessorKey: 'visibility', header: 'Visibility', size: 100 },
- { accessorKey: 'created_at', header: 'Uploaded At', size: 150, Cell: ({ cell }) => getFormattedDate(cell.getValue()) },
+ { accessorKey:'file_path', header:'File Path', size:200 },
+ { accessorKey:'file_type', header:'Type', size:100 },
+ { accessorKey:'visibility', header:'Visibility', size:100 },
+ {
+ accessorKey:'created_at',
+ header:'Uploaded At',
+ size:150,
+ Cell:( { cell } ) => getFormattedDate( cell.getValue() )
+ },
];
const postColumns = [
- { accessorKey: 'title', header: 'Title', size: 200 },
- { accessorKey: 'post_type', header: 'Type', size: 100 },
- { accessorKey: 'visibility', header: 'Visibility', size: 100 },
- { accessorKey: 'created_at', header: 'Posted At', size: 150, Cell: ({ cell }) => getFormattedDate(cell.getValue()) },
+ { accessorKey:'title', header:'Title', size:200 },
+ { accessorKey:'post_type', header:'Type', size:100 },
+ { accessorKey:'visibility', header:'Visibility', size:100 },
+ {
+ accessorKey:'created_at',
+ header:'Posted At',
+ size:150,
+ Cell:( { cell } ) => getFormattedDate( cell.getValue() )
+ },
];
const groupColumns = [
- { accessorKey: 'name', header: 'Group Name', size: 200 },
- { accessorKey: 'created_by_id', header: 'Created By', size: 100 },
- { accessorKey: 'created_at', header: 'Created At', size: 150, Cell: ({ cell }) => getFormattedDate(cell.getValue()) },
+ { accessorKey:'name', header:'Group Name', size:200 },
+ { accessorKey:'created_by_id', header:'Created By', size:100 },
+ {
+ accessorKey:'created_at',
+ header:'Created At',
+ size:150,
+ Cell:( { cell } ) => getFormattedDate( cell.getValue() )
+ },
];
+ const messageTable = useMaterialReactTable( {
+ columns:messageColumns,
+ data:recentMessages,
+ enableColumnActions:false,
+ enableColumnFilters:false,
+ enablePagination:false,
+ enableSorting:false,
+ muiTablePaperProps:{
+ className:'shadow-none',
+ },
+ muiTableHeadCellProps:{
+ className:'table-header',
+ },
+ muiTableBodyCellProps:{
+ className:'table-row',
+ }
+ } );
+
+ const mediaTable = useMaterialReactTable( {
+ columns:mediaColumns,
+ data:recentMedia,
+ enableColumnActions:false,
+ enableColumnFilters:false,
+ enablePagination:false,
+ enableSorting:false,
+ muiTablePaperProps:{
+ className:'shadow-none',
+ },
+ muiTableHeadCellProps:{
+ className:'table-header',
+ },
+ muiTableBodyCellProps:{
+ className:'table-row',
+ }
+ } );
+
+ const postTable = useMaterialReactTable( {
+ columns:postColumns,
+ data:recentPosts,
+ enableColumnActions:false,
+ enableColumnFilters:false,
+ enablePagination:false,
+ enableSorting:false,
+ muiTablePaperProps:{
+ className:'shadow-none',
+ },
+ muiTableHeadCellProps:{
+ className:'table-header',
+ },
+ muiTableBodyCellProps:{
+ className:'table-row',
+ }
+ } );
+
+ const groupTable = useMaterialReactTable( {
+ columns:groupColumns,
+ data:recentGroups,
+ enableColumnActions:false,
+ enableColumnFilters:false,
+ enablePagination:false,
+ enableSorting:false,
+ muiTablePaperProps:{
+ className:'shadow-none',
+ },
+ muiTableHeadCellProps:{
+ className:'table-header',
+ },
+ muiTableBodyCellProps:{
+ className:'table-row',
+ }
+ } );
+
return (
- <Container maxWidth="xl" className="container">
- <Typography variant="h4" className="text-3xl font-bold mb-6 mt-4">
- Welcome to PHS Admin{user ? `, ${user.first_name || 'User ' + user.id}` : ''}
+ <Container maxWidth="xxl">
+ <Typography variant="h4"
+ className="text-3xl font-bold mb-6 mt-4">
+ Welcome to PHS Admin{ user ? `, ${ user.first_name || 'User ' + user.id }` : '' }
</Typography>
- <Grid container spacing={3}>
- <Grid item xs={12} md={6}>
- <Card className="shadow-md rounded-lg">
+ <Grid container
+ spacing={ 2 }
+ className="w-full">
+ <Grid item
+ size={ { xs:12, md:6 } }>
+ <Card className="shadow-md rounded-lg w-full">
<CardContent>
- <Typography variant="h6" className="font-semibold mb-2">
+ <Typography variant="h6"
+ className="font-semibold mb-2">
Recent Messages
</Typography>
- <MaterialReactTable
- columns={messageColumns}
- data={recentMessages}
- enableColumnActions={false}
- enableColumnFilters={false}
- enablePagination={false}
- enableSorting={false}
- muiTablePaperProps={{ className: 'shadow-none' }}
- muiTableHeadCellProps={{ className: 'table-header' }}
- muiTableBodyCellProps={{ className: 'table-row' }}
- />
- <Box mt={2}>
- <Button variant="contained" color="primary" onClick={() => navigate('/messages')}>
+ <MaterialReactTable table={ messageTable }/>
+ <Box mt={ 2 }>
+ <Button variant="contained"
+ color="primary"
+ onClick={ () => navigate( '/messages' ) }>
View All Messages
</Button>
</Box>
</CardContent>
</Card>
</Grid>
- <Grid item xs={12} md={6}>
+ <Grid item
+ size={ { xs:12, md:6 } }>
<Card className="shadow-md rounded-lg">
<CardContent>
- <Typography variant="h6" className="font-semibold mb-2">
+ <Typography variant="h6"
+ className="font-semibold mb-2">
Recent Media
</Typography>
- <MaterialReactTable
- columns={mediaColumns}
- data={recentMedia}
- enableColumnActions={false}
- enableColumnFilters={false}
- enablePagination={false}
- enableSorting={false}
- muiTablePaperProps={{ className: 'shadow-none' }}
- muiTableHeadCellProps={{ className: 'table-header' }}
- muiTableBodyCellProps={{ className: 'table-row' }}
- />
- <Box mt={2}>
- <Button variant="contained" color="primary" onClick={() => navigate('/media')}>
+ <MaterialReactTable table={ mediaTable }/>
+ <Box mt={ 2 }>
+ <Button variant="contained"
+ color="primary"
+ onClick={ () => navigate( '/media' ) }>
View All Media
</Button>
</Box>
</CardContent>
</Card>
</Grid>
- <Grid item xs={12} md={6}>
+ <Grid item
+ size={ { xs:12, md:6 } }>
<Card className="shadow-md rounded-lg">
<CardContent>
- <Typography variant="h6" className="font-semibold mb-2">
+ <Typography variant="h6"
+ className="font-semibold mb-2">
Recent Posts
</Typography>
- <MaterialReactTable
- columns={postColumns}
- data={recentPosts}
- enableColumnActions={false}
- enableColumnFilters={false}
- enablePagination={false}
- enableSorting={false}
- muiTablePaperProps={{ className: 'shadow-none' }}
- muiTableHeadCellProps={{ className: 'table-header' }}
- muiTableBodyCellProps={{ className: 'table-row' }}
- />
- <Box mt={2}>
- <Button variant="contained" color="primary" onClick={() => navigate('/posts')}>
+ <MaterialReactTable table={ postTable }/>
+ <Box mt={ 2 }>
+ <Button variant="contained"
+ color="primary"
+ onClick={ () => navigate( '/posts' ) }>
View All Posts
</Button>
</Box>
</CardContent>
</Card>
</Grid>
- <Grid item xs={12} md={6}>
+ <Grid item
+ size={ { xs:12, md:6 } }>
<Card className="shadow-md rounded-lg">
<CardContent>
- <Typography variant="h6" className="font-semibold mb-2">
+ <Typography variant="h6"
+ className="font-semibold mb-2">
Recent Message Groups
</Typography>
- <MaterialReactTable
- columns={groupColumns}
- data={recentGroups}
- enableColumnActions={false}
- enableColumnFilters={false}
- enablePagination={false}
- enableSorting={false}
- muiTablePaperProps={{ className: 'shadow-none' }}
- muiTableHeadCellProps={{ className: 'table-header' }}
- muiTableBodyCellProps={{ className: 'table-row' }}
- />
- <Box mt={2}>
- <Button variant="contained" color="primary" onClick={() => navigate('/message-groups')}>
+ <MaterialReactTable table={ groupTable }/>
+ <Box mt={ 2 }>
+ <Button variant="contained"
+ color="primary"
+ onClick={ () => navigate( '/message-groups' ) }>
View All Groups
</Button>
</Box>