diff --git a/app/components/troi.client.tsx b/app/components/troi.client.tsx index 8ec5d6b3..230cf1f9 100644 --- a/app/components/troi.client.tsx +++ b/app/components/troi.client.tsx @@ -13,7 +13,6 @@ import moment from "moment"; import { TimeEntries, TimeEntry } from "~/troi/TimeEntry"; import { CalculationPosition } from "~/troi/CalculationPosition"; import { useFetcher } from "@remix-run/react"; -import { convertToCacheFormat } from "~/utils/TimeEntryCache"; import { LoadingOverlay } from "./LoadingOverlay"; interface Props { @@ -88,9 +87,9 @@ export default function Troi(props: Props) { }, { method: "POST", - action: `/calculation_postions/${ - position.id - }/time_entries/${convertToCacheFormat(selectedDate)}`, + action: `/calculation_postions/${position.id}/time_entries/${moment( + selectedDate, + ).format("YYYY-MM-DD")}`, }, ); } diff --git a/app/routes/calculation_postions.$calculationPositionId.time_entries.$date.tsx b/app/routes/calculation_postions.$calculationPositionId.time_entries.$date.tsx index c443e288..728efaf4 100644 --- a/app/routes/calculation_postions.$calculationPositionId.time_entries.$date.tsx +++ b/app/routes/calculation_postions.$calculationPositionId.time_entries.$date.tsx @@ -1,5 +1,5 @@ import { ActionFunctionArgs } from "@remix-run/node"; -import { addTimeEntry } from "~/troi/troiControllerServer"; +import { addTimeEntry } from "~/troi/troiApiController"; export async function action({ request, params }: ActionFunctionArgs) { if (request.method !== "POST") { diff --git a/app/routes/projects.tsx b/app/routes/projects.tsx index f99c66b0..42008931 100644 --- a/app/routes/projects.tsx +++ b/app/routes/projects.tsx @@ -13,7 +13,7 @@ import { getCalculationPositions, getCalenderEvents, getTimeEntries, -} from "~/troi/troiControllerServer"; +} from "~/troi/troiApiController"; let isHydrating = true; diff --git a/app/routes/time_entries.$id.tsx b/app/routes/time_entries.$id.tsx index 265dd274..e58edfc6 100644 --- a/app/routes/time_entries.$id.tsx +++ b/app/routes/time_entries.$id.tsx @@ -1,5 +1,5 @@ import { ActionFunctionArgs } from "@remix-run/node"; -import { deleteTimeEntry, updateTimeEntry } from "~/troi/troiControllerServer"; +import { deleteTimeEntry, updateTimeEntry } from "~/troi/troiApiController"; export async function action({ request, params }: ActionFunctionArgs) { if (!params.id) { diff --git a/app/troi/troiControllerServer.ts b/app/troi/troiApiController.ts similarity index 100% rename from app/troi/troiControllerServer.ts rename to app/troi/troiApiController.ts diff --git a/app/troi/troiController.ts b/app/troi/troiController.ts deleted file mode 100644 index 131aefa4..00000000 --- a/app/troi/troiController.ts +++ /dev/null @@ -1,335 +0,0 @@ -import TimeEntryCache, { convertToCacheFormat } from "../utils/TimeEntryCache"; -import { - addDaysToDate, - formatDateToYYYYMMDD, - getWeekDaysFor, -} from "../utils/dateUtils"; -import type { TransformedCalendarEvent } from "../utils/transformCalendarEvents"; -import { transformCalendarEvent } from "../utils/transformCalendarEvents"; -import type { TimeEntry } from "troi-library"; -import type TroiApiService from "troi-library"; - -const timeEntryCache = new TimeEntryCache(); - -const intervallInWeeks = 6; -const intervallInDays = intervallInWeeks * 7; - -export type Project = { - name: string; - id: number; - subproject: number; -}; - -class TroiApiNotInitializedError extends Error { - constructor(message?: string, options?: ErrorOptions) { - super(message ?? "TroiAPI not initialized", options); - } -} - -export default class TroiController { - private _troiApi?: TroiApiService; - private _startLoadingCallback?: () => unknown; - private _stopLoadingCallback?: () => unknown; - private currentWeek = getWeekDaysFor(new Date()); - private _cacheBottomBorder: Date = addDaysToDate( - this.currentWeek[0], - -intervallInDays, - ); - private _cacheTopBorder: Date = addDaysToDate( - this.currentWeek[4], - intervallInDays, - ); - private _projects?: Project[]; - - async init( - troiApi: TroiApiService, - willStartLoadingCallback: () => unknown, - finishedLoadingCallback: () => unknown, - ) { - this._troiApi = troiApi; - this._startLoadingCallback = willStartLoadingCallback; - this._stopLoadingCallback = finishedLoadingCallback; - - await this._troiApi.initialize(); - - this._projects = await this._troiApi - .makeRequest({ - url: "/calculationPositions", - params: { - clientId: (await this._troiApi.getClientId()).toString(), - favoritesOnly: true.toString(), - }, - }) - .then((response) => - (response as any).map((obj: unknown) => { - return { - name: (obj as any).DisplayPath, - id: (obj as any).Id, - subproject: (obj as any).Subproject.id, - }; - }), - ); - - await this._loadEntriesAndEventsBetween( - this._cacheBottomBorder, - this._cacheTopBorder, - ); - } - - // --------- private functions --------- - - // TODO: remove - // quick fix when clientId or employeeId is undefined - _verifyTroiApiCredentials() { - if (this._troiApi === undefined) { - throw new TroiApiNotInitializedError(); - } - - if ( - this._troiApi.clientId == undefined || - this._troiApi.employeeId == undefined - ) { - console.log( - "clientId:", - this._troiApi?.clientId, - "employeeId:", - this._troiApi?.employeeId, - ); - alert("An error in Troi occured, please reload track-your-time!"); - return false; - } - return true; - } - - async _loadEntriesBetween(startDate: Date, endDate: Date) { - if (this._troiApi === undefined) { - throw new TroiApiNotInitializedError(); - } - - // TODO: remove quick fix - if (!this._verifyTroiApiCredentials) { - return; - } - - await Promise.all( - this._projects?.map(async (project) => { - if (this._troiApi === undefined) { - throw new TroiApiNotInitializedError(); - } - console.log( - "employeeId", - this._troiApi.employeeId, - "projectId", - project.id, - ); - const entries = await this._troiApi.getTimeEntries( - project.id, - formatDateToYYYYMMDD(startDate), - formatDateToYYYYMMDD(endDate), - ); - - timeEntryCache.addEntries(project, entries); - }) ?? [], - ); - } - - async _loadCalendarEventsBetween(startDate: Date, endDate: Date) { - if (this._troiApi === undefined) { - throw new TroiApiNotInitializedError(); - } - - // TODO: remove quick fix - if (!this._verifyTroiApiCredentials) { - return; - } - const calendarEvents = await this._troiApi.getCalendarEvents( - formatDateToYYYYMMDD(startDate), - formatDateToYYYYMMDD(endDate), - ); - - calendarEvents.forEach((calendarEvent) => { - const transformedEvents = transformCalendarEvent( - calendarEvent, - startDate, - endDate, - ); - transformedEvents.forEach((event) => { - timeEntryCache.addEvent(event); - }); - }); - } - - async _loadEntriesAndEventsBetween(startDate: Date, endDate: Date) { - if (this._troiApi === undefined) { - throw new TroiApiNotInitializedError(); - } - - // might be quick fix for not loading time entries if employeeId is undefined - if (this._troiApi.employeeId == undefined) { - return; - } - - await this._loadEntriesBetween(startDate, endDate); - await this._loadCalendarEventsBetween(startDate, endDate); - - this._cacheBottomBorder = new Date( - Math.min( - new Date(this._cacheBottomBorder).getTime(), - startDate.getTime(), - ), - ); - this._cacheTopBorder = new Date( - Math.max(new Date(this._cacheTopBorder).getTime(), endDate.getTime()), - ); - } - - // ------------------------------------- - - getProjects() { - return this._projects; - } - - getTimesAndEventsFor(week: Date[]) { - let timesAndEventsOfWeek: { - hours: number; - events: TransformedCalendarEvent[]; - }[] = []; - - week.forEach((date) => { - timesAndEventsOfWeek.push({ - hours: timeEntryCache.totalHoursOf(date), - events: timeEntryCache.getEventsFor(date), - }); - }); - - return timesAndEventsOfWeek; - } - - getEventsFor(date: Date) { - return timeEntryCache.getEventsFor(date); - } - - // CRUD Functions for entries - - // CRUD Functions for entries - - async getEntriesFor(date: Date) { - if (date > this._cacheTopBorder) { - this._startLoadingCallback?.(); - const fetchStartDate = getWeekDaysFor(date)[0]; - const fetchEndDate = addDaysToDate(fetchStartDate, intervallInDays - 3); - - await this._loadEntriesAndEventsBetween(fetchStartDate, fetchEndDate); - this._stopLoadingCallback?.(); - } - - if (date < this._cacheBottomBorder) { - this._startLoadingCallback?.(); - const fetchEndDate = getWeekDaysFor(date)[4]; - const fetchStartDate = addDaysToDate(fetchEndDate, -intervallInDays + 3); - - await this._loadEntriesAndEventsBetween(fetchStartDate, fetchEndDate); - this._stopLoadingCallback?.(); - } - - return timeEntryCache.getEntriesFor(date); - } - - async addEntry( - date: Date, - project: Project, - hours: number, - description: string, - successCallback: () => unknown, - ) { - if (this._troiApi === undefined) { - throw new TroiApiNotInitializedError(); - } - // TODO: remove quick fix - if (!this._verifyTroiApiCredentials) { - return; - } - this._startLoadingCallback?.(); - const troiFormattedSelectedDate = convertToCacheFormat(date); - const result = (await this._troiApi.postTimeEntry( - project.id, - troiFormattedSelectedDate, - hours, - description, - )) as { - Name: string; - Quantity: string; - Id: number; - }; - - const entry: TimeEntry = { - date: troiFormattedSelectedDate, - description: result.Name, - hours: Number(result.Quantity), - id: result.Id, - }; - - timeEntryCache.addEntry(project, entry, successCallback); - this._stopLoadingCallback?.(); - } - - async deleteEntry( - entry: TimeEntry, - projectId: number, - successCallback: () => unknown, - ) { - if (this._troiApi === undefined) { - throw new TroiApiNotInitializedError(); - } - // TODO: remove quick fix - if (!this._verifyTroiApiCredentials) { - return; - } - this._startLoadingCallback?.(); - let result = (await this._troiApi.deleteTimeEntryViaServerSideProxy( - entry.id, - )) as { - ok: boolean; - }; - if (result.ok) { - timeEntryCache.deleteEntry(entry, projectId, successCallback); - } - this._stopLoadingCallback?.(); - } - - async updateEntry( - project: Project, - entry: TimeEntry, - successCallback: () => unknown, - ) { - if (this._troiApi === undefined) { - throw new TroiApiNotInitializedError(); - } - // TODO: remove quick fix - if (!this._verifyTroiApiCredentials) { - return; - } - this._startLoadingCallback?.(); - const result = (await this._troiApi.updateTimeEntry( - project.id, - entry.date, - entry.hours, - entry.description, - entry.id, - )) as { - Name: string; - Quantity: string; - Id: number; - }; - - const updatedEntry = { - date: entry.date, - description: result.Name, - hours: Number(result.Quantity), - id: result.Id, - }; - - timeEntryCache.updateEntry(project, updatedEntry, successCallback); - this._stopLoadingCallback?.(); - } -} diff --git a/app/troi/useTroi.hook.tsx b/app/troi/useTroi.hook.tsx deleted file mode 100644 index ac6e9e15..00000000 --- a/app/troi/useTroi.hook.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useEffect, useState } from "react"; -import TroiController from "../troi/troiController"; -import TroiApiService from "troi-library"; - -export function useTroi(username: string, password: string) { - // note: this pattern could lead to race conditions when password or username change - const [troiController, setTroiController] = useState< - TroiController | undefined - >(); - const [loading, setLoading] = useState(false); - const [initialized, setInitialized] = useState(false); - - useEffect(() => { - const contoller = new TroiController(); - contoller - .init( - new TroiApiService({ - baseUrl: "https://digitalservice.troi.software/api/v2/rest", - clientName: "DigitalService GmbH des Bundes", - username, - password, - }), - () => { - setLoading(true); - }, - () => { - setLoading(false); - }, - ) - .then(() => { - setTroiController(contoller); - setInitialized(true); - }); - - return () => { - setInitialized(false); - setTroiController(undefined); - }; - }, [username, password]); - - return { - troiController, - loading: loading || !initialized, - initialized, - }; -} diff --git a/app/utils/TimeEntryCache.ts b/app/utils/TimeEntryCache.ts deleted file mode 100644 index 3a974cfb..00000000 --- a/app/utils/TimeEntryCache.ts +++ /dev/null @@ -1,284 +0,0 @@ -import moment from "moment"; -import type { CalculationPosition, TimeEntry } from "troi-library"; -import type { TransformedCalendarEvent } from "./transformCalendarEvents"; - -// cache structure -/* - '2023-03-13': { - projects: { - 254: { - name: Grundsteuer, - entries: [ - {id: 14694, date: '2023-03-13', hours: 1}, - {id: 14695, date: '2023-03-13', hours: 2}, - ... - ] - }, - 255: { - name: useID, - entries: [ - ... - ] - } - }, - sum: 3, - events : [ - { - id: '12821', - subject: 'Karfreitag', - type: 'H', - startTime: 09:00:00, - endTime: 18:00:00 - } - ] - }, - ... -*/ - -const intervallInWeeks = 6; -const intervallInDays = intervallInWeeks * 7; - -export default class TimeEntryCache { - private cache: { - [data: string]: { - projects: { - [projectId: number]: { - entries: TimeEntry[]; - name: string; - }; - }; - events: TransformedCalendarEvent[]; - sum: number; - }; - }; - - topBorder: number; - bottomBorder: number; - weekIndex: number; - - constructor() { - this.cache = {}; - this.topBorder = intervallInWeeks; - this.bottomBorder = -intervallInWeeks; - this.weekIndex = 0; - } - - // TODO: rework to private property - getIntervallInDays() { - return intervallInDays; - } - - // ----------------------- - // internal cache functions - _getDay(date: string) { - return this.cache[date]; - } - - _entriesFor(date: string, projectId: number) { - return this.cache[date]["projects"][projectId]["entries"]; - } - - _findEntryWithSameDescription(entry: TimeEntry, projectId: number) { - return this._entriesFor(entry.date, projectId).find( - (e) => e.description.toLowerCase() == entry.description.toLowerCase(), - ); - } - - _projectsFor(date: Date | string) { - const cacheDate = convertToCacheFormat(date); - if (cacheDate in this.cache) { - return this._getDay(cacheDate)["projects"]; - } else { - return {}; - } - } - // ----------------------- - - getEntriesFor(date: Date) { - const cacheDate = convertToCacheFormat(date); - let entriesForDate: { - [projectId: number]: TimeEntry[]; - } = {}; - - if (this.cache[cacheDate] == undefined) { - return entriesForDate; - } - - Object.keys(this.cache[cacheDate]["projects"]).forEach((projectId) => { - entriesForDate[parseInt(projectId, 10)] = this.entriesForProject( - date, - parseInt(projectId, 10), - ); - }); - - return entriesForDate; - } - - getEventsFor(date: Date) { - const cacheDate = convertToCacheFormat(date); - if (this.cache[cacheDate] == undefined) { - return []; - } - - return this.cache[cacheDate]["events"]; - } - - addEntries(project: CalculationPosition, entries: TimeEntry[]) { - entries.forEach((entry) => { - this.addEntry(project, entry); - }); - } - - //eslint-disable-next-line @typescript-eslint/no-empty-function - addEntry( - position: CalculationPosition, - entry: TimeEntry, - successCallback = () => {}, - ) { - // init if not present - this.initStructureForDateIfNotPresent(entry.date); - - let projects = this._projectsFor(entry.date); - // init if not present - if (!(position.id in projects)) { - this.initStructureForProject(entry.date, position); - } - - // if description already exists, delete "old entry" bc. troi api adds hours to entry and does not create new one - const existingEntry = this._findEntryWithSameDescription( - entry, - position.id, - ); - if (existingEntry) { - this.deleteEntry(existingEntry, position.id); - } - - this._entriesFor(entry.date, position.id).push(entry); - this._getDay(entry.date).sum += entry.hours; - successCallback(); - } - - //eslint-disable-next-line @typescript-eslint/no-empty-function - deleteEntry(entry: TimeEntry, projectId: number, successCallback = () => {}) { - const entries = this._entriesFor(entry.date, projectId); - const index = entries.map((entry) => entry.id).indexOf(entry.id); - - // If the element is not found, simply return - if (index == -1) { - return; - } - entries.splice(index, 1); - - this.aggregateHoursFor(entry.date); - successCallback(); - } - - //eslint-disable-next-line @typescript-eslint/no-empty-function - updateEntry( - project: CalculationPosition, - entry: TimeEntry, - successCallback = () => {}, - ) { - // if description already exists, delete "old entry" bc. troi api adds hours to entry and does not create new one - const existingEntry = this._findEntryWithSameDescription(entry, project.id); - if (existingEntry) { - this.deleteEntry(existingEntry, project.id); - } - this.deleteEntry(entry, project.id); - this.addEntry(project, entry); - successCallback(); - } - - addEvent(event: TransformedCalendarEvent) { - const cacheDate = convertToCacheFormat(event.date); - this.initStructureForDateIfNotPresent(cacheDate); - this.cache[cacheDate]["events"].push(event); - } - - initStructureForDateIfNotPresent(date: string) { - if (this.cache[date]) { - return; - } - - this.cache[date] = { - projects: {}, - sum: 0, - events: [], - }; - } - - initStructureForProject(date: Date | string, position: CalculationPosition) { - this._projectsFor(date)[position.id] = { - entries: [], - name: position.name, - }; - } - - aggregateHoursFor(date: string) { - // get all projectIds - const projectIds = Object.keys(this._projectsFor(date)); - - // iterate entries in each project and aggregate hours - let sum = 0; - projectIds.forEach((projectId) => { - sum += this._entriesFor(date, parseInt(projectId, 10)).reduce( - (accumulator, entry) => { - return accumulator + entry.hours; - }, - 0, - ); - }); - - // assign hours - this.cache[date].sum = sum; - } - - increaseBottomBorderByIntervall() { - this.bottomBorder -= intervallInWeeks; - } - - increaseTopBorderByIntervall() { - this.topBorder += intervallInWeeks; - } - - isAtCacheBottom() { - return this.weekIndex == this.bottomBorder; - } - - isAtCacheTop() { - return this.weekIndex == this.topBorder; - } - - increaseWeekIndex() { - this.weekIndex++; - } - - decreaseWeekIndex() { - this.weekIndex--; - } - - entriesForProject(date: Date, projectId: number) { - const cacheDate = convertToCacheFormat(date); - if ( - cacheDate in this.cache && - projectId in this._getDay(cacheDate)["projects"] - ) { - return this._getDay(cacheDate)["projects"][projectId]["entries"]; - } else { - return []; - } - } - - totalHoursOf(date: Date) { - const cacheDate = convertToCacheFormat(date); - if (cacheDate in this.cache) { - return this.cache[cacheDate].sum; - } else { - return 0; - } - } -} - -export function convertToCacheFormat(date: Date | string) { - return moment(date).format("YYYY-MM-DD"); -}