diff --git a/src/App.tsx b/src/App.tsx index a877867..0edc2aa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 ( <> diff --git a/src/AppRouting.tsx b/src/AppRouting.tsx index d09d1db..920c468 100644 --- a/src/AppRouting.tsx +++ b/src/AppRouting.tsx @@ -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'; @@ -115,6 +116,16 @@ const AppRouting = () => { } /> + + }> + + + + } + /> ); }; diff --git a/src/components/Charts.tsx b/src/components/Charts.tsx index 17d8c68..61cd1a6 100644 --- a/src/components/Charts.tsx +++ b/src/components/Charts.tsx @@ -42,6 +42,7 @@ const Charts = () => { { + const notifyContext = useNotification().notifyContext; + + const { profile, user } = useAppSelector((state) => state.auth); + const { times, evaluateTimes } = useAppSelector((state) => state.times); + + const [days, setDays] = useState([]); + const [filteredTimes, setFilteredTimes] = useState(null); + const [timesExport, setTimesExport] = useState(null); + const [downloadBlocked, setDownloadBlocked] = useState(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 ( + + + + + + Der aktuell Monat führt zu nicht korrekten Werten, da die Daten noch nicht vollständig sind. Bitte wähle einen vergangenen Monat + aus. + + + {timesExport && ( + + )} + + + ); +}; + +export default ExportPage; diff --git a/src/components/UserHomePage.tsx b/src/components/UserHomePage.tsx index 8ba8c19..db23524 100644 --- a/src/components/UserHomePage.tsx +++ b/src/components/UserHomePage.tsx @@ -107,7 +107,8 @@ const UserHomePage = () => { - + {profile && } + = ({ children }) => { + + + + + + + + diff --git a/src/components/time/TimeHistoryPage.tsx b/src/components/time/TimeHistoryPage.tsx index e29c900..16e0ff1 100644 --- a/src/components/time/TimeHistoryPage.tsx +++ b/src/components/time/TimeHistoryPage.tsx @@ -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([]); const [historyTimes, setHistoryTimes] = useState(null); const [workingTimeGlz, setWorkingTimeGlz] = useState(null); const [availableTimeGlz, setAvailableTimeGlz] = useState(null); - const [timesExport, setTimesExport] = useState(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. @@ -70,7 +35,6 @@ const TimeHistoryPage = () => { }); setWorkingTimeGlz(TimeUtils.minutesToTime(workingTimeSum)); setAvailableTimeGlz(TimeUtils.minutesToTime(availableTimeSum)); - prepareExportData(); } }, [historyTimes]); @@ -106,11 +70,6 @@ const TimeHistoryPage = () => { )} {days && days.map((day) => )} - {timesExport && ( - - )} ); diff --git a/src/interfaces/StoreTypes.ts b/src/interfaces/StoreTypes.ts index f276ac1..d1976fe 100644 --- a/src/interfaces/StoreTypes.ts +++ b/src/interfaces/StoreTypes.ts @@ -10,6 +10,7 @@ export interface ITimesContext { sumLastMonth: ITimeDistribution; firstTimeDay: Dayjs | null; evaluateTimes: IBalance[]; + timesLastRefresh: number; } export interface ITimeDistribution { @@ -21,6 +22,7 @@ export interface IAbsencesContext { absences: IAbsence[]; holidays: IAbsence[]; sickDays: IAbsence[]; + absencesLastRefresh: number; } export interface IBalance { diff --git a/src/store/slices/absencesSlice.ts b/src/store/slices/absencesSlice.ts index 815b085..6ebf636 100644 --- a/src/store/slices/absencesSlice.ts +++ b/src/store/slices/absencesSlice.ts @@ -5,6 +5,7 @@ const initialState: IAbsencesContext = { absences: [], holidays: [], sickDays: [], + absencesLastRefresh: 0, }; const absencesSlice = createSlice({ @@ -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; diff --git a/src/store/slices/timesSlice.ts b/src/store/slices/timesSlice.ts index 3b7c12e..0b2e2e2 100644 --- a/src/store/slices/timesSlice.ts +++ b/src/store/slices/timesSlice.ts @@ -10,6 +10,7 @@ const initialState: ITimesContext = { sumLastMonth: { workingTime: 0, availableTime: 0 }, firstTimeDay: null, evaluateTimes: [], + timesLastRefresh: 0, }; export const timesSlice = createSlice({ @@ -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; diff --git a/src/store/thunks/absencesThunks.ts b/src/store/thunks/absencesThunks.ts index 326b9c9..ce304cf 100644 --- a/src/store/thunks/absencesThunks.ts +++ b/src/store/thunks/absencesThunks.ts @@ -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; @@ -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) => { diff --git a/src/store/thunks/timesThunks.ts b/src/store/thunks/timesThunks.ts index cb30387..85202a7 100644 --- a/src/store/thunks/timesThunks.ts +++ b/src/store/thunks/timesThunks.ts @@ -1,4 +1,5 @@ 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 { IBalance } from '../../interfaces/StoreTypes'; @@ -13,6 +14,7 @@ import { setSumMonth, setSumYear, setTimes, + setTimesLastRefresh, setTimesLoading, } from '../slices/timesSlice'; @@ -45,6 +47,7 @@ export const getTimes = createAsyncThunk('times/all', async (action: any, thunkA // Set times and loading state thunkAPI.dispatch(setTimes(result)); thunkAPI.dispatch(setTimesLoading(false)); + thunkAPI.dispatch(setTimesLastRefresh(dayjs().unix())); }); export const addTimes = createAsyncThunk('times/add', async (action: any, thunkAPI: any) => { @@ -115,15 +118,15 @@ export const evaluateTimes = createAsyncThunk('evaluate/times', async (action: a continue; } - // calculate workdays in the current month - const workDaysInMonth = TimeUtils.workDaysInMonth(from, to, profile); - result['days'] = {}; - result['days']['total'] = workDaysInMonth; - // filter absences for the current month const absencesInMonth = AbsenceUtils.getAbsencesByTimestampRange(absences, from, to); + result['days'] = {}; result['days']['absences'] = absencesInMonth.length; + // calculate workdays in the current month + const workDaysInMonth = TimeUtils.workDaysInMonth(from, to, profile); + result['days']['total'] = workDaysInMonth; + // calculate target values for the current month const targetDaysInMonth = workDaysInMonth - absencesInMonth.length; result['days']['personal'] = targetDaysInMonth; @@ -153,5 +156,6 @@ export const evaluateTimes = createAsyncThunk('evaluate/times', async (action: a dataset.push(result); } + console.log(dataset); thunkAPI.dispatch(setEvaluateTimes(dataset)); }); diff --git a/src/utils/HolidayUtils.ts b/src/utils/HolidayUtils.ts index 70407dc..f52af90 100644 --- a/src/utils/HolidayUtils.ts +++ b/src/utils/HolidayUtils.ts @@ -4,6 +4,7 @@ import DayjsUtils from './DayjsUtils'; import holidaysJson from './Holidays.json'; import TimeUtils from './TimeUtils'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars export const checkIsWorkday = (workingdays: IWorkingdays, day: Dayjs, state: string) => { const workday = TimeUtils.checkWorkday(day, workingdays); const holiday = isHoliday(day, state); diff --git a/src/utils/PDFExport.ts b/src/utils/PDFExport.ts index 3c2a65e..3a2b2b8 100644 --- a/src/utils/PDFExport.ts +++ b/src/utils/PDFExport.ts @@ -1,14 +1,59 @@ -import jsPDF from 'jspdf'; +import { jsPDF } from 'jspdf'; import autoTable, { CellDef } from 'jspdf-autotable'; import { ITimeCsv } from '../interfaces/Types'; +import TimeUtils from './TimeUtils'; const header = [ ['Kalenderwoche', 'Datum', 'Arbeitszeit (h)', 'Arbeitszeit Saldo (h)', 'Verfügungszeit (min)', 'Verfügungszeit Saldo (min)', 'Notizen'], ]; -export const exportPdf = (data: ITimeCsv[]) => { +export const exportPdf = ( + data: ITimeCsv[], + fileName: string, + month: string, + name: string, + formerWorkingTimeBalance: number, + formerAvailableTimeBalance: number, + currentWorkingTimeBalance: number, + currentAvailableTimeBalance: number +) => { const doc = new jsPDF(); + // Set the title at the top of the page + doc.setFontSize(18); + doc.text('Monatsbericht', 14, 22); // Position the title + + // Add additional information + doc.setFontSize(10); + doc.text(`Monat: ${month}`, 14, 30); + doc.text(`Name: ${name}`, 14, 35); + + doc.setFontSize(12); + doc.text('Vormonat Salden in (h):', 14, 45); // Increase the y value here + + // Add underline + let textSize = doc.getTextDimensions('Vormonat Salden in (h):'); + doc.line(14, 46, 14 + textSize.w, 46); // Increase the y values here + + doc.setFontSize(10); + doc.text(`Arbeitszeit: ${TimeUtils.negativeMinutesToTime(formerWorkingTimeBalance)}`, 14, 50); // Increase the y value here + doc.text(`Verfügungszeit: ${TimeUtils.negativeMinutesToTime(formerAvailableTimeBalance)}`, 14, 55); // Increase the y value here + + // Add current month balances + doc.setFontSize(12); + doc.text('Monat Salden in (h):', 14, 65); // Increase the y value here + + // Add underline + textSize = doc.getTextDimensions('Monat Salden in (h):'); + doc.line(14, 66, 14 + textSize.w, 66); // Increase the y values here + + doc.setFontSize(10); + doc.text(`Arbeitszeit: ${TimeUtils.negativeMinutesToTime(currentWorkingTimeBalance)}`, 14, 70); // Increase the y value here + doc.text(`Verfügungszeit: ${TimeUtils.negativeMinutesToTime(currentAvailableTimeBalance)}`, 14, 75); // Increase the y value here + + // Define starting position for the tables after the title + let startY = 80; // Increase the y value here + const weeks = new Set(data.map((item) => item.week)); weeks.forEach((week) => { @@ -52,10 +97,15 @@ export const exportPdf = (data: ITimeCsv[]) => { autoTable(doc, { head: header, body: body, + startY: startY, }); + + // Update startY for the next table to avoid overlap + const finalY = (doc as any).lastAutoTable.finalY; + startY = finalY + 10; }); - doc.save('zeiten.pdf'); + doc.save(fileName); }; export default { exportPdf };