Skip to content

Commit

Permalink
Add more charts to home page
Browse files Browse the repository at this point in the history
  • Loading branch information
Florian Jud committed May 16, 2024
1 parent 9eb1d97 commit d490ad5
Show file tree
Hide file tree
Showing 13 changed files with 720 additions and 78 deletions.
391 changes: 391 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@mui/icons-material": "^5.15.17",
"@mui/material": "^5.15.17",
"@mui/x-date-pickers": "^7.4.0",
"@mui/x-charts": "^7.4.0",
"@reduxjs/toolkit": "^2.2.4",
"@types/node": "^20.12.12",
"@types/react": "^18.3.2",
Expand Down
8 changes: 7 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { useAppDispatch, useAppSelector } from './store/hooks';
import { setUser } from './store/slices/authSlice';
import { getAbsences } from './store/thunks/absencesThunks';
import { loadProfile } from './store/thunks/authThunks';
import { getTimes } from './store/thunks/timesThunks';
import { evaluateTimes, getTimes } from './store/thunks/timesThunks';
import A2HSInstaller from './utils/A2HSInstaller';
import AnalyticsTracker from './utils/AnalyticsTracker';
import { darkThemeOptions, lightThemeOptions } from './utils/MyThemeOptions';
Expand Down Expand Up @@ -72,6 +72,12 @@ const App = () => {
return unsubscribe;
}, []);

const times = useAppSelector((state) => state.times.times);

useEffect(() => {
if (times && times.length > 0) dispatch(evaluateTimes(undefined));
}, [times]);

return (
<>
<ThemeProvider theme={appliedTheme}>
Expand Down
55 changes: 55 additions & 0 deletions src/components/Charts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Stack, Typography } from '@mui/material';
import { LineChart } from '@mui/x-charts';
import { BarChart } from '@mui/x-charts/BarChart';
import { useAppSelector } from '../store/hooks';
import DayjsUtils from '../utils/DayjsUtils';

const Charts = () => {
const evaluateTimes = useAppSelector((state) => state.times.evaluateTimes);

const dataset = evaluateTimes.map((item) => ({
month: DayjsUtils.monthStringByNumber(item.month),
workingTime: (item.workingTime?.balance || 0) / 60,
availableTime: (item.availableTime?.balance || 0) / 60,
}));

return (
<>
{evaluateTimes && (
<Stack direction="column" alignItems="center" spacing={2} pt={4}>
<Stack direction="column" alignItems="center" spacing={0}>
<Typography variant="h6">Verfügungszeit-Saldo je Monat (Std.)</Typography>
<BarChart
dataset={dataset}
xAxis={[{ scaleType: 'band', dataKey: 'month' }]}
yAxis={[
{
colorMap: {
type: 'piecewise',
thresholds: [0],
colors: ['#ffa680', '#49a7cc'],
},
},
]}
series={[{ dataKey: 'availableTime' }]}
width={window.innerWidth * 0.8}
height={300}
borderRadius={18}
/>
</Stack>
<Stack direction="column" alignItems="center" spacing={0}>
<Typography variant="h6">Arbeitszeit-Saldo je Monat (Std.)</Typography>
<LineChart
dataset={dataset}
xAxis={[{ scaleType: 'band', dataKey: 'month' }]}
series={[{ dataKey: 'workingTime' }]}
width={window.innerWidth * 0.8}
height={300}
/>
</Stack>
</Stack>
)}
</>
);
};
export default Charts;
90 changes: 33 additions & 57 deletions src/components/UserHomePage.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import AccessTimeOutlinedIcon from '@mui/icons-material/AccessTimeOutlined';
import AirplanemodeActiveIcon from '@mui/icons-material/AirplanemodeActive';
import PsychologyAltOutlinedIcon from '@mui/icons-material/PsychologyAltOutlined';
import SickOutlinedIcon from '@mui/icons-material/SickOutlined';
import { Divider, Grid, Stack, Typography } from '@mui/material';
import { Dayjs } from 'dayjs';
import locale from 'dayjs/locale/de';
import { useEffect, useState } from 'react';
import packageJson from '../../package.json';
import { useAppSelector } from '../store/hooks';
import { ReactComponent as HomeSvg } from '../svg/home.svg';
import AbsenceUtils from '../utils/AbsenceUtils';
import DayjsUtils from '../utils/DayjsUtils';
import HolidayUtils from '../utils/HolidayUtils';
import TimeUtils from '../utils/TimeUtils';
import Charts from './Charts';
import UserHomePageInfoCard from './UserHomePageInfoCard';
import VacationGauge from './VacationGauge';

const UserHomePage = () => {
const { user, profile } = useAppSelector((state) => state.auth);
Expand All @@ -22,14 +21,13 @@ const UserHomePage = () => {

const [noHolidays, setNoHolidays] = useState<number>(0);
const [noSickDays, setNoSickDays] = useState<number>(0);
const [noAbsences, setNoAbsences] = useState<number>(0);
const [noAbsencesInMonth, setNoAbsencesInMonth] = useState<number>(0);
const [balanceWorkMinutes, setBalanceWorkMinutes] = useState<number>(0);
const [balanceAvailableMinutes, setBalanceAvailableMinutes] = useState<number>(0);
const [workDaysThisMonth, setWorkDaysThisMonth] = useState<number>(0);

const [myStartfrom, setMyStartfrom] = useState<Dayjs | null>(null);
const to = DayjsUtils.endOfCurrentYear();
const today = DayjsUtils.todayStart();
const firstDayOfMonth = DayjsUtils.firstDayOfCurrentMonth();
const startOfCurrentYear = DayjsUtils.startOfCurrentYear();

Expand All @@ -39,10 +37,10 @@ const UserHomePage = () => {
Set the first day of the year as the start date
*/
useEffect(() => {
if(firstTimeDay){
if(firstTimeDay.isAfter(startOfCurrentYear)){
if (firstTimeDay) {
if (firstTimeDay.isAfter(startOfCurrentYear)) {
setMyStartfrom(firstTimeDay);
}else{
} else {
setMyStartfrom(startOfCurrentYear);
}
}
Expand All @@ -61,78 +59,55 @@ const UserHomePage = () => {
setNoHolidays(profile.holidays);
}

// get all absences
const resAbsences = AbsenceUtils.getAbsencesByTimestampRange(absences, myStartfrom.unix(), to.unix());
if (resAbsences) setNoAbsences(resAbsences.length);

// absences in this month
const resAbsencesThisMonth = AbsenceUtils.getAbsencesByTimestampRange(absences, firstDayOfMonth.unix(), today.unix());
const resAbsencesThisMonth = AbsenceUtils.getAbsencesByTimestampRange(absences, firstDayOfMonth.unix(), DayjsUtils.todayEnd().unix());
if (resAbsencesThisMonth) setNoAbsencesInMonth(resAbsencesThisMonth.length);

// calculate sickness Days for this year
const resSickDays = AbsenceUtils.getSickDaysByTimestampRange(sickDays, myStartfrom.unix(), to.unix());
setNoSickDays(resSickDays.length);

// Calculate personal work days this month
const resWorkDaysMonth = TimeUtils.workDaysTillTodayInMonthByStartDate(firstDayOfMonth, profile);
setWorkDaysThisMonth(resWorkDaysMonth);
}
}, [profile, myStartfrom]);

/*
Calculate Saldo Work Time
Calculate Saldo Work Time and Available Time
*/
useEffect(() => {
// calculate Saldo Work Time
if (sumMonth.workingTime && profile) {
const days: Dayjs[] = [];
const d = Math.ceil(today.diff(firstDayOfMonth, 'day', true));
for (let i = 0; i < d; i++) {
const nextDay = firstDayOfMonth.add(i, 'day').locale({ ...locale });
if (HolidayUtils.checkIsWorkday(profile.workingdays, nextDay, profile.state)) {
days.push(nextDay);
}
}
const targetWorkingDays = days.length - noAbsences;
const targetWorkingMinutes = targetWorkingDays * profile.workingtime;
setBalanceWorkMinutes(sumMonth.workingTime - targetWorkingMinutes);
}
}, [sumMonth.workingTime, myStartfrom]);
if (sumMonth && profile) {
const targetDays = workDaysThisMonth - noAbsencesInMonth;

/*
Calculate Available Time
*/
useEffect(() => {
// calculate Available Time
if (sumMonth.availableTime && profile) {
const days: Dayjs[] = [];
const d = Math.ceil(today.diff(firstDayOfMonth, 'day', true));
for (let i = 0; i < d; i++) {
const nextDay = firstDayOfMonth.add(i, 'day').locale({ ...locale });
if (HolidayUtils.checkIsWorkday(profile.workingdays, nextDay, profile.state)) {
days.push(nextDay);
}
}
const targetAvailableDays = days.length - noAbsencesInMonth;
const targetAvailableMinutes = targetAvailableDays * profile.availabletime;
const targetWorkingMinutes = targetDays * profile.workingtime;
const targetAvailableMinutes = targetDays * profile.availabletime;

setBalanceWorkMinutes(sumMonth.workingTime - targetWorkingMinutes);
setBalanceAvailableMinutes(sumMonth.availableTime - targetAvailableMinutes);
}
}, [sumMonth.availableTime, myStartfrom]);
}, [sumMonth, workDaysThisMonth]);

return (
<Stack direction="column" alignItems="center" spacing={0}>
<HomeSvg width="150px" height="150px" />

<Typography variant="h4" gutterBottom>
Dashboard
<Typography
variant="h4"
gutterBottom
sx={{
background: 'linear-gradient(to right, #49a7cc, #ffa680)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}}>
Hallo {user?.displayName}
</Typography>
<Typography variant="body1" align="center" gutterBottom>
Willkommen zuück, {user?.displayName}! Wir haben dich vermisst. 👋
👋 Willkommen zuück! Wir haben dich 😻 vermisst.
</Typography>
<Divider sx={{ my: 2 }} />
<Grid direction="row" justifyContent="center" alignItems="center" gap={4} container>
<UserHomePageInfoCard
title="Freie Urlaubstage"
value={`${noHolidays} Tg.`}
subtitle="In diesem Jahr"
SvgIcon={AirplanemodeActiveIcon}
/>
<VacationGauge value={noHolidays} valueMax={profile?.holidays || 0} />
<UserHomePageInfoCard
title="Arbeitszeit am Kind Saldo"
value={`${TimeUtils.negativeMinutesToTime(balanceWorkMinutes)} h`}
Expand All @@ -147,8 +122,9 @@ const UserHomePage = () => {
/>
<UserHomePageInfoCard title="Krankheitstage" value={`${noSickDays} Tg.`} subtitle="In diesem Jahr" SvgIcon={SickOutlinedIcon} />
</Grid>
<Typography variant="caption" color="text.disabled" align="center" gutterBottom>
v{APPLICATION_VERSION}
<Charts />
<Typography variant="caption" color="text.disabled" align="center" pt={2}>
v{APPLICATION_VERSION}
</Typography>
</Stack>
);
Expand Down
32 changes: 32 additions & 0 deletions src/components/VacationGauge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import AirplanemodeActiveIcon from '@mui/icons-material/AirplanemodeActive';
import { Card, CardContent, Stack, Typography } from '@mui/material';
import { Gauge } from '@mui/x-charts';

interface VacationGaugePropertioes {
value: number;
valueMax: number;
}

const VacationGauge = ({ value, valueMax }: VacationGaugePropertioes) => {
return (
<Card sx={{ minWidth: 256 }}>
<CardContent>
<Stack direction="column" justifyContent="flex-start" alignItems="flex-start" gap={2} width="100%">
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" gap={2} width="100%">
<Typography variant="body1" color="text.secondary">
Freie Urlaubstage
</Typography>
<AirplanemodeActiveIcon color="secondary" />
</Stack>

<Gauge value={value} startAngle={-110} endAngle={110} height={100} text={() => `${value} / ${valueMax}`} />
<Typography variant="body2" color="text.disabled">
In diesem Jahr
</Typography>
</Stack>
</CardContent>
</Card>
);
};

export default VacationGauge;
28 changes: 28 additions & 0 deletions src/interfaces/StoreTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface ITimesContext {
sumMonth: ITimeDistribution;
sumLastMonth: ITimeDistribution;
firstTimeDay: Dayjs | null;
evaluateTimes: IBalance[];
}

export interface ITimeDistribution {
Expand All @@ -21,3 +22,30 @@ export interface IAbsencesContext {
holidays: IAbsence[];
sickDays: IAbsence[];
}

export interface IBalance {
year: number;
month: number;
range: {
from: number;
to: number;
};

days?: {
total?: number;
personal?: number;
absences?: number;
};

workingTime?: {
target?: number;
actual?: number;
balance?: number;
};

availableTime?: {
target?: number;
actual?: number;
balance?: number;
};
}
2 changes: 2 additions & 0 deletions src/interfaces/Types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,5 @@ export interface IBeforeInstallPromptEvent extends Event {
}>;
prompt(): Promise<void>;
}

export type MonthName = 'Jan' | 'Feb' | 'Mar' | 'Apr' | 'May' | 'Jun' | 'Jul' | 'Aug' | 'Sep' | 'Oct' | 'Nov' | 'Dec';
7 changes: 6 additions & 1 deletion src/store/slices/timesSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const initialState: ITimesContext = {
sumMonth: { workingTime: 0, availableTime: 0 },
sumLastMonth: { workingTime: 0, availableTime: 0 },
firstTimeDay: null,
evaluateTimes: [],
};

export const timesSlice = createSlice({
Expand All @@ -33,12 +34,16 @@ export const timesSlice = createSlice({
setFirstTimeDay: (state, action) => {
state.firstTimeDay = action.payload;
},
setEvaluateTimes: (state, action) => {
state.evaluateTimes = action.payload;
},
},
extraReducers: (builder) => {
builder.addCase(getTimes.fulfilled, () => {});
},
});

export const { setTimes, setTimesLoading, setSumYear, setSumMonth, setSumLastMonth, setFirstTimeDay } = timesSlice.actions;
export const { setTimes, setTimesLoading, setSumYear, setSumMonth, setSumLastMonth, setFirstTimeDay, setEvaluateTimes } =
timesSlice.actions;

export default timesSlice.reducer;
Loading

0 comments on commit d490ad5

Please sign in to comment.