Web Application zur Verwaltung erfasster Zeiten

Abrechnungen werden bei avocado software engineering mit einer eigens dafür erstellten Web Application erstellt. Auch intern vereinfachen wir Abläufe stetig und gestalten sie somit effizienter und smarter. Das bereits bestehende Abrechnungstool sollte durch eine neuere Version abgelöst werden.

Das Softwaretool deckt die folgenden drei Hauptbereiche ab:

  1. Abrechnungen für Kunden sollen im Tool erstellt werden.
  2. Einzelne Zeiteinträge von Redmine (unser Ticket-Managementsystem) sollen neuen Abrechnungen zugeordnet werden.
  3. Die noch nicht abgerechneten Zeiten und somit offenen Zeiten sollen eingesehen und verwaltet werden.

Oberfläche des Tools

Das Dashboard

Über das Dashboard können bequem neue Abrechnungen erstellt werden, oder vorhandene Abrechnungen bearbeitet werden. In dieser Ansicht ist es auch möglich sich eine Excel-Datei für eine vorhandene Abrechnung erzeugen zu lassen. Diese Excel-Liste mit allen Arbeitszeiteinträgen im gewünschten Zeitraum geht schlussendlich an unsere Kunden zusammen mit einer Rechnung heraus.

Erstellen einer neuen Abrechnung

Durch Auswahl des gewünschten Projekts, sowie des gewünschten Zeitraums, werden alle der Auswahl entsprechenden Zeiteinträge von Redmine in die Ansicht geladen. Mithilfe der Checkboxen auf der linken Seite können die einzelnen Zeiteinträge der Abrechnung hinzugefügt oder von der Abrechnung entfernt werden.

Die offenen Zeiten

Alle Zeiten, die noch nicht abgerechnet wurden, gehören zu den offenen Zeiten. Die offenen Zeiten werden nach Auswahl des gewünschten Zeitraums in einer Tabelle nach Projekt sortiert angezeigt. Über einen Button kann eine neue Abrechnung mit den noch offenen Zeiten des Projekts erstellt werden.

Technische Ausführung

Zeiteinträge von Redmine laden

Die Zeiten eines Projekts werden in der frei verfügbaren Software Redmine gespeichert. Wenn ein Zeitraum angegeben wird, und das abzurechnende Projekt ausgewählt ist, werden mit einem Aufruf an die Redmine-API die Zeiteinträge im JSON-Format geladen und in der Tabelle angezeigt. Alternativ zu der Wahl eines Zeitraums kann auch die gewünschte Anzahl an Projekttagen ausgewählt werden. Dafür werden ab dem jüngsten Zeiteintrag der Datenbank bis zum Tag der Erstellung der Abrechnung die Zeiteinträge geladen und danach nach Projekttagen gefiltert.

public getTimeEntriesWithDate(startDate: Date, endDate: Date, projectId: number, offset: number): Observable<any> {

    // +1 Weil -1 Tag ausgewählt wird wegen ISO
    const sDate: Date = new Date(startDate);
    this.addDays(sDate, 1);

    const eDate: Date = new Date(endDate);
    this.addDays(eDate, 1);

    const startDateFormat = sDate.toISOString().substr(0, 10);
    const endDateFormat = eDate.toISOString().substr(0, 10);

    return this.http.get(this.url + '/time_entries.json?' + 'project_id=' + projectId + '&from=' + startDateFormat +
        '&to=' + endDateFormat + '&offset=' + offset + '&limit=100', this.getHttpOptions());
}

Die Zeiteinträge von Redmine sind folgendermaßen aufgebaut.

{
   activity: Object { id: 9, name: "Entwicklung" }
   comments: "Es wurde eine Liste von Heroes hinzugefügt, wobei bei einem ausgewählten Hero nähere Informationen angezeigt werden."
   created_on: "2021-09-01T15:36:00Z"
   custom_fields: Array [ {…} ]
   hours: 2.2
   id: 63056
   issue: Object { id: 18478 }
   project: Object { id: 168, name: "Heroes" }
   spent_on: "2021-09-01"
   updated_on: "2021-09-02T07:40:33Z"
   user: Object { id: 39, name: "Uwe Sänger" }
}

   comments: Die Beschreibung der Tätigkeit

   spent_on: Datum, an welchem die Zeit aufgewendet wurde

   hours: Die aufgewendete Zeit in Stunden

   project: Welchem Projekt die Zeit zugeordnet ist

   user: Wer den Zeiteintrag erstellt hat

Speichern einer Abrechnung

Wenn im Frontend vom User eine neue Abrechnung mit Zeiteinträgen erstellt wird, muss diese in der Datenbank abgespeichert werden. Hierfür gibt es ein Model, das wiederum ein Model für die Abrechnung und eine Liste von Models für die Zeiteinträge beinhaltet. Die Daten der Abrechnung werden im JSON-Format an das auf Java (Spring) basierende Backend versendet und dort in der Datenbank gespeichert. Die Datenbank enthält zwei für das Abrechnungstool wichtige Tabellen. Die Tabelle invoices für die Abrechnungen und die Tabelle time_entries für die Zeiteinträge, die einen Fremdschlüssel auf die Tabelle invoices besitzt, sodass die Zeiteinträge eindeutig einer Abrechnung zuordenbar sind.

public Boolean add(InvoiceWithTimeEntries invoiceWithTimeEntries) {

    LocalDateTime today = LocalDateTime.now();
    java.sql.Date todaySql = java.sql.Date.valueOf(today.toLocalDate());

    boolean successfulInsert = dslContext.insertInto(INVOICES)
            .set(INVOICES.CREATED_AT, todaySql)
            .set(INVOICES.ASSIGNED_TIME, invoiceWithTimeEntries.get_entries().length)
            .set(INVOICES.NAME, invoiceWithTimeEntries.get_invoice().getName())
            .set(INVOICES.PROJECT_ID, invoiceWithTimeEntries.get_invoice().getProjectId())
            .set(INVOICES.SUM_HOURS, (double) invoiceWithTimeEntries.get_invoice().getSumHours())
            .set(INVOICES.STATUS, invoiceWithTimeEntries.get_invoice().getStatus())
            .execute() > 0;

    if (!successfulInsert) {
        return false;
    }

    int id = dslContext.lastID().intValue();

    List<UpdatableRecord<?>> recordList = new ArrayList<>();

    for (TimeEntries timeEntries : invoiceWithTimeEntries.get_entries()) {

        LocalDate createdOnLocal = this.convertToLocalDateViaInstant(timeEntries.getSpent_on());
        java.sql.Date createdOn = java.sql.Date.valueOf(createdOnLocal);

        UpdatableRecord<?> newRecord = (UpdatableRecord<?>) dslContext.newRecord(TIME_ENTRIES);
        newRecord.set(TIME_ENTRIES.USERNAME, timeEntries.getUser().getName());
        newRecord.set(TIME_ENTRIES.HOURS, timeEntries.getHours());
        newRecord.set(TIME_ENTRIES.ACTIVITY, timeEntries.getActivity().getName());
        newRecord.set(TIME_ENTRIES.COMMENTS, timeEntries.getComments());
        newRecord.set(TIME_ENTRIES.DATE, createdOn);
        newRecord.set(TIME_ENTRIES.INVOICES_ID, id);
        newRecord.set(TIME_ENTRIES.ID_REDMINE, timeEntries.getId());
        recordList.add(newRecord);
    }

    dslContext.batchStore(recordList).execute();
    return true;
}

Filtern der offenen Zeiten

Um für jedes Projekt die noch offenen / noch nicht abgerechneten Zeiten anzeigen zu können, muss die Software für jedes Projekt die schon abgerechneten Zeiteinträge aus der Datenbank laden und alle Zeiteinträge von Redmine laden. Anschließend werden für jedes Projekt die Einträge von Redmine und die Einträge aus der Datenbank verglichen und nur die nicht in der Datenbank vorkommenden Zeiteinträge werden für die offenen Zeiten verwendet.

public filterOpenEntries(): void {
    this.dbEntries.forEach((tupel, index) => {
       tupel[1].forEach(entry => {
         const tmpindex = this.redmineEntries[index][1].findIndex(timeEntry => (timeEntry.id === entry.redmineId));
         if (tmpindex >= 0) {
            this.redmineEntries[index][1].splice(tmpindex, 1);
         }
       });
    });
    this.setDaysAndHours();
}

Die Arbeit im Frontend und im Backend des Abrechnungstools erlaubte es mir die Tätigkeiten eines Softwareentwicklers bei avocado näher kennen zu lernen und von Anfang an Projektverantwortlichkeit übernehmen zu dürfen.

Uwe Sänger (Student Angewandte Informatik, 5. Semester)

Veröffentlicht am 18. Januar 2022