Skip to content

Commit

Permalink
Extend PDF Export and fix data calculation
Browse files Browse the repository at this point in the history
  • Loading branch information
Florian Jud committed May 16, 2024
1 parent dbf6eb8 commit 24d8ea7
Show file tree
Hide file tree
Showing 14 changed files with 242 additions and 59 deletions.
11 changes: 8 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,16 @@ const App = () => {
return unsubscribe;
}, []);

const times = useAppSelector((state) => state.times.times);
const { times, timesLastRefresh } = useAppSelector((state) => state.times);
const absencesLastRefresh = useAppSelector((state) => state.absences.absencesLastRefresh);

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

dispatch(evaluateTimes(undefined));
}, [timesLastRefresh, absencesLastRefresh]);

return (
<>
Expand Down
11 changes: 11 additions & 0 deletions src/AppRouting.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { LinearProgress } from '@mui/material';
import { Suspense, lazy } from 'react';
import { Route, Routes } from 'react-router';
import ExportPage from './components/ExportPage';
import ProtectedRoute from './components/common/ProtectedRoute';
import TimeEditPage from './components/time/TimeEditPage';

Expand Down Expand Up @@ -115,6 +116,16 @@ const AppRouting = () => {
</ProtectedRoute>
}
/>
<Route
path="/export"
element={
<ProtectedRoute>
<Suspense fallback={<LinearProgress color="secondary" />}>
<ExportPage />
</Suspense>
</ProtectedRoute>
}
/>
</Routes>
);
};
Expand Down
1 change: 1 addition & 0 deletions src/components/Charts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const Charts = () => {
<LineChart
dataset={dataset}
xAxis={[{ scaleType: 'band', dataKey: 'month' }]}
yAxis={[{ min: 0, max: 100 }]}
series={[{ dataKey: 'workingTime' }]}
width={window.innerWidth * 0.8}
height={300}
Expand Down
122 changes: 122 additions & 0 deletions src/components/ExportPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { Download } from '@mui/icons-material';
import { Button, Stack, Typography } from '@mui/material';
import { Dayjs } from 'dayjs';
import { useEffect, useState } from 'react';
import useNotification from '../hooks/useNotification';
import { ITime, ITimeCsv } from '../interfaces/Types';
import { useAppSelector } from '../store/hooks';
import { ReactComponent as TimesSvg } from '../svg/airport.svg';
import DayjsUtils from '../utils/DayjsUtils';
import { exportPdf } from '../utils/PDFExport';
import TimeUtils from '../utils/TimeUtils';
import MainContainer from './common/MainContainer';
import MonthPicker from './common/MonthPicker';

const ExportPage = () => {
const notifyContext = useNotification().notifyContext;

const { profile, user } = useAppSelector((state) => state.auth);
const { times, evaluateTimes } = useAppSelector((state) => state.times);

const [days, setDays] = useState<Dayjs[]>([]);
const [filteredTimes, setFilteredTimes] = useState<ITime[] | null>(null);
const [timesExport, setTimesExport] = useState<ITimeCsv[] | null>(null);
const [downloadBlocked, setDownloadBlocked] = useState<boolean>(true);

const handleDateRangePicker = (dates: Dayjs[]) => setDays(dates);

/*
load all times from Firestore DB, either the select month,
or the current month, if nothing selected yet.
*/
useEffect(() => {
if (days && days.length > 0) {
const [from, to] = DayjsUtils.calculateFromAndTo(days);
if (times && times.length > 0) {
const res = TimeUtils.filterByTimestampRange(from.unix(), to.unix(), times);
setFilteredTimes(res);
setDownloadBlocked(false);
}
}
}, [days]);

useEffect(() => {
if (filteredTimes && filteredTimes.length > 0) {
const data: ITimeCsv[] = [];
filteredTimes.forEach((time) => {
let workingTimeBalance = 0;
let availableTimeBalance = 0;
if (time.workday) {
workingTimeBalance = time.workingTime - (profile?.workingtime || 0);
availableTimeBalance = time.availableTime - (profile?.availabletime || 0);
} else {
workingTimeBalance = time.workingTime;
availableTimeBalance = time.availableTime;
}

data.push({
week: TimeUtils.dateStringToWeek(time.day),
day: time.day,
workingTime: time.workingTime,
workingTimeBalance: workingTimeBalance,
availableTime: time.availableTime,
availableTimeBalance: availableTimeBalance,
notes: time.notes,
});
});
setTimesExport(data);
}
}, [filteredTimes]);

const handleDownload = () => {
if (!timesExport) {
notifyContext.addNotification('Keine Daten zum Exportieren gefunden', 'error');
return;
}
const firstDay = days[0];
const month = days[0].month();
const fileName = `report_${firstDay.format('MM-YYYY')}.pdf`;
const exportMonth = firstDay.format('MMMM YYYY');
const name = user?.displayName || '';

// get the balance of the last month
const previsousAvailableTimeBalance = evaluateTimes[month - 1]?.availableTime?.balance || 0;
const previsousWorkingTimeBalance = evaluateTimes[month - 1]?.workingTime?.balance || 0;

// get the balance of the current month
const availableTimeBalanceCurrent = evaluateTimes[month]?.availableTime?.balance || 0;
const workingTimeBalanceCurrent = evaluateTimes[month]?.workingTime?.balance || 0;

exportPdf(
timesExport,
fileName,
exportMonth,
name,
previsousWorkingTimeBalance,
previsousAvailableTimeBalance,
workingTimeBalanceCurrent,
availableTimeBalanceCurrent
);
};

return (
<MainContainer>
<Stack direction="column" alignItems="center" spacing={2}>
<TimesSvg width="150px" height="150px" />
<MonthPicker onChange={handleDateRangePicker} />
<Typography variant="caption">
Der aktuell Monat führt zu nicht korrekten Werten, da die Daten noch nicht vollständig sind. Bitte wähle einen vergangenen Monat
aus.
</Typography>

{timesExport && (
<Button disabled={downloadBlocked} variant="contained" endIcon={<Download />} onClick={handleDownload}>
Bericht herunterladen
</Button>
)}
</Stack>
</MainContainer>
);
};

export default ExportPage;
3 changes: 2 additions & 1 deletion src/components/UserHomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ const UserHomePage = () => {
</Typography>
<Divider sx={{ my: 2 }} />
<Grid direction="row" justifyContent="center" alignItems="center" gap={4} container>
<VacationGauge value={noHolidays} valueMax={profile?.holidays || 0} />
{profile && <VacationGauge value={profile.holidays - noHolidays} valueMax={profile.holidays} />}

<UserHomePageInfoCard
title="Arbeitszeit am Kind Saldo"
value={`${TimeUtils.negativeMinutesToTime(balanceWorkMinutes)} h`}
Expand Down
9 changes: 9 additions & 0 deletions src/components/menu/NavigationDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Menu,
Person,
Subscriptions,
Summarize,
} from '@mui/icons-material';
import {
Box,
Expand Down Expand Up @@ -177,6 +178,14 @@ const NavigationDrawer: FC<Props> = ({ children }) => {
<ListItemText primary="Zeiten" />
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton component={Link} to="/export" onClick={handleDrawerClose}>
<ListItemIcon>
<Summarize />
</ListItemIcon>
<ListItemText primary="Bericht" />
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton component={Link} to="/time/add" onClick={handleDrawerClose}>
<ListItemIcon>
Expand Down
45 changes: 2 additions & 43 deletions src/components/time/TimeHistoryPage.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,26 @@
import DownloadIcon from '@mui/icons-material/Download';
import { Button, Paper, Stack, Typography } from '@mui/material';
import { Paper, Stack, Typography } from '@mui/material';
import { Dayjs } from 'dayjs';
import { useEffect, useState } from 'react';
import { ITime, ITimeCsv } from '../../interfaces/Types';
import { ITime } from '../../interfaces/Types';
import { useAppSelector } from '../../store/hooks';
import { ReactComponent as TimesSvg } from '../../svg/times.svg';
import DayjsUtils from '../../utils/DayjsUtils';
import { exportPdf } from '../../utils/PDFExport';
import TimeUtils from '../../utils/TimeUtils';
import MainContainer from '../common/MainContainer';
import MonthPicker from '../common/MonthPicker';
import ViewDate from './TimeHistoryCard';

const TimeHistoryPage = () => {
const profile = useAppSelector((state) => state.auth.profile);
const times = useAppSelector((state) => state.times.times);

const [days, setDays] = useState<Dayjs[]>([]);
const [historyTimes, setHistoryTimes] = useState<ITime[] | null>(null);
const [workingTimeGlz, setWorkingTimeGlz] = useState<string | null>(null);
const [availableTimeGlz, setAvailableTimeGlz] = useState<string | null>(null);
const [timesExport, setTimesExport] = useState<ITimeCsv[] | null>(null);

// Date Range Picker callback function to set the selected date range
const handleDateRangePicker = (dates: Dayjs[]) => setDays(dates);

/*
Prepare the data for the PDF export.
*/
const prepareExportData = () => {
if (historyTimes) {
const data: ITimeCsv[] = [];
historyTimes.forEach((time) => {
let workingTimeBalance = 0;
let availableTimeBalance = 0;
if (time.workday) {
workingTimeBalance = time.workingTime - (profile?.workingtime || 0);
availableTimeBalance = time.availableTime - (profile?.availabletime || 0);
} else {
workingTimeBalance = time.workingTime;
availableTimeBalance = time.availableTime;
}

data.push({
week: TimeUtils.dateStringToWeek(time.day),
day: time.day,
workingTime: time.workingTime,
workingTimeBalance: workingTimeBalance,
availableTime: time.availableTime,
availableTimeBalance: availableTimeBalance,
notes: time.notes,
});
});
setTimesExport(data);
}
};

/*
Simply sumup all working and available times reveived
from Firestore DB and convert from a number to time string.
Expand All @@ -70,7 +35,6 @@ const TimeHistoryPage = () => {
});
setWorkingTimeGlz(TimeUtils.minutesToTime(workingTimeSum));
setAvailableTimeGlz(TimeUtils.minutesToTime(availableTimeSum));
prepareExportData();
}
}, [historyTimes]);

Expand Down Expand Up @@ -106,11 +70,6 @@ const TimeHistoryPage = () => {
</Paper>
)}
{days && days.map((day) => <ViewDate key={day.toISOString()} day={day} />)}
{timesExport && (
<Button variant="contained" endIcon={<DownloadIcon />} onClick={() => exportPdf(timesExport)}>
Download
</Button>
)}
</Stack>
</MainContainer>
);
Expand Down
2 changes: 2 additions & 0 deletions src/interfaces/StoreTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface ITimesContext {
sumLastMonth: ITimeDistribution;
firstTimeDay: Dayjs | null;
evaluateTimes: IBalance[];
timesLastRefresh: number;
}

export interface ITimeDistribution {
Expand All @@ -21,6 +22,7 @@ export interface IAbsencesContext {
absences: IAbsence[];
holidays: IAbsence[];
sickDays: IAbsence[];
absencesLastRefresh: number;
}

export interface IBalance {
Expand Down
6 changes: 5 additions & 1 deletion src/store/slices/absencesSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const initialState: IAbsencesContext = {
absences: [],
holidays: [],
sickDays: [],
absencesLastRefresh: 0,
};

const absencesSlice = createSlice({
Expand All @@ -20,9 +21,12 @@ const absencesSlice = createSlice({
setSickDays: (state, action) => {
state.sickDays = action.payload;
},
setAbsencesLastRefresh: (state, action) => {
state.absencesLastRefresh = action.payload;
},
},
});

export const { setAbsences, setHolidays, setSickDays } = absencesSlice.actions;
export const { setAbsences, setHolidays, setSickDays, setAbsencesLastRefresh } = absencesSlice.actions;

export default absencesSlice.reducer;
16 changes: 14 additions & 2 deletions src/store/slices/timesSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const initialState: ITimesContext = {
sumLastMonth: { workingTime: 0, availableTime: 0 },
firstTimeDay: null,
evaluateTimes: [],
timesLastRefresh: 0,
};

export const timesSlice = createSlice({
Expand Down Expand Up @@ -37,13 +38,24 @@ export const timesSlice = createSlice({
setEvaluateTimes: (state, action) => {
state.evaluateTimes = action.payload;
},
setTimesLastRefresh: (state, action) => {
state.timesLastRefresh = action.payload;
},
},
extraReducers: (builder) => {
builder.addCase(getTimes.fulfilled, () => {});
},
});

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

export default timesSlice.reducer;
4 changes: 3 additions & 1 deletion src/store/thunks/absencesThunks.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import dayjs from 'dayjs';
import { collection, deleteDoc, doc, getDocs, query, where, writeBatch } from 'firebase/firestore';
import { db } from '../../firebase/Firebase';
import { IAbsence } from '../../interfaces/Types';
import AbsenceUtils from '../../utils/AbsenceUtils';
import TimeUtils from '../../utils/TimeUtils';
import { setAbsences, setHolidays, setSickDays } from '../slices/absencesSlice';
import { setAbsences, setAbsencesLastRefresh, setHolidays, setSickDays } from '../slices/absencesSlice';

export const getAbsences = createAsyncThunk('absences/all', async (action: any, thunkAPI: any) => {
const { user } = thunkAPI.getState().auth;
Expand All @@ -29,6 +30,7 @@ export const getAbsences = createAsyncThunk('absences/all', async (action: any,
thunkAPI.dispatch(setSickDays(sickDays));

thunkAPI.dispatch(setAbsences(result));
thunkAPI.dispatch(setAbsencesLastRefresh(dayjs().unix()));
});

export const addAbsences = createAsyncThunk('absences/add', async (action: any, thunkAPI: any) => {
Expand Down
Loading

0 comments on commit 24d8ea7

Please sign in to comment.