Compare commits

..
5 Commits
Author SHA1 Message Date
bjoernpoettkerandClaude Opus 5.5 9bda65eec0 Eingangsrechnungen: Buchungsstatus aus #bookedStatus lesen
Seit AM hinter dem Buchungsstatus "Zahlung erfassen" anzeigt, lieferte der
letzte Teil der Statuszeile kein Buchungsdatum mehr. Fallback fürs alte
Layout sucht den Teil mit "gebucht".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 14:02:07 +01:00
bjoernpoettkerandClaude Opus 5.5 659fdb5293 Arbeitszeit: Arbeitstag beginnen, beenden, aendern und loeschen
beginneArbeitstag legt per saveSchicht.php eine laufende Schicht fuer
beliebige Mitarbeiter an (der Menuepunkt "Arbeitstag beginnen" wirkt nur
fuer den angemeldeten Benutzer). Damit kann auch eine Pause die erste
Buchung des Tages sein. Zeitbuchungen lassen laufende Schichten offen
und ziehen nur den Beginn bei Bedarf vor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:54:58 +02:00
bjoernpoettkerandClaude Opus 5.5 d703e46125 Arbeitszeit: Zeitbuchungen anlegen, aendern und loeschen
createZeitbuchung, updateZeitbuchung und deleteZeitbuchung buchen
Taetigkeitsbloecke und Pausen ueber module/arbeitszeit/api/. Die erste
Buchung eines Tages legt per nachtragen.php die Schicht an, weitere
Buchungen kommen in die vorhandene Schicht, die bei Bedarf verlaengert
wird. Die Kostenstelle wird an die Notiz gehaengt. Unbekannte
Taetigkeiten werden vorher abgelehnt, weil Agrarmonitor sie ohne Fehler
speichert. Ablehnungen kommen als ZeitbuchungError mit Grund.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:42:57 +02:00
bjoernpoettkerandClaude Opus 5.5 6db6c5b278 Stammdaten: getTaetigkeiten und getAlleMaschinen
getTaetigkeiten liest alle Taetigkeiten (aktiv und inaktiv) mit Bereich
und den Flags Maschinen zuordnen, Abwesenheit und Notiz erzwingen.
getAlleMaschinen liest alle Kategorien und Seiten und fasst Maschinen,
die in mehreren Kategorien stehen, zu einem Eintrag zusammen.
Paging ueber .pagination[data-pages], Abbruch sobald eine Seite keine
neuen Zeilen bringt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:11:25 +02:00
bjoernpoettkerandClaude Opus 5.5 d2c5db7e22 Arbeitszeit: Analyse der Zeitbuchung in Agrarmonitor
Fundstellen fuer Taetigkeiten, Maschinen und Kostenstellen sowie die
Buchungsendpunkte unter module/arbeitszeit/api/ (Nachtragen, Verteilung
anlegen/aendern/loeschen, Schicht) mit Feldern, Antworten und offenen
Fragen fuer die Poettker-App.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:11:23 +02:00
23 changed files with 3110 additions and 12 deletions

No files matched your search

+9
View File
@@ -97,9 +97,18 @@ The store can read both the connector format and the older Telefonbuch cookie-ar
- `setEingangsdatum(rechnungId, datum)` updates the received date. - `setEingangsdatum(rechnungId, datum)` updates the received date.
- `getMaschinenKategorien()` reads `/maschinen/kategorien` and returns ID, name, tractor flag, and mobile visibility. - `getMaschinenKategorien()` reads `/maschinen/kategorien` and returns ID, name, tractor flag, and mobile visibility.
- `getMaschinen(gruppenId, suchstring?)` reads `/module/maschinen/livesearch.php` and returns ID, number, name, license plate, and active flag. - `getMaschinen(gruppenId, suchstring?)` reads `/module/maschinen/livesearch.php` and returns ID, number, name, license plate, and active flag.
- `getAlleMaschinen()` reads all machine categories (`maschinengruppe=0`), active and inactive machines, all livesearch pages, and returns each machine once with ID, number, name, license plate, active flag, its first category (`gruppenId`, `gruppe`) and all categories (`gruppenIds`); a machine can be listed in several categories. `getMaschinen()` already sends `maschinen_aktiv=1`, which in Agrarmonitor means active **and** inactive.
- `getFirmen()` reads `/einstellungen` and returns company ID and description. - `getFirmen()` reads `/einstellungen` and returns company ID and description.
- `getArtikelEinheiten()` reads `/artikel/einheiten` and returns unit ID, description, and short label. - `getArtikelEinheiten()` reads `/artikel/einheiten` and returns unit ID, description, and short label.
- `getArtikelKategorien()` reads `/artikel/kategorien` and returns product group ID and name. - `getArtikelKategorien()` reads `/artikel/kategorien` and returns product group ID and name.
- `getArtikel(artikelGruppe, suchstring?)` reads `/module/artikel/livesearch.php` and returns article ID, number, and description. - `getArtikel(artikelGruppe, suchstring?)` reads `/module/artikel/livesearch.php` and returns article ID, number, and description.
- `getMitarbeiter(suchstring?, mitarbeitergruppe?)` reads `/module/mitarbeiter/livesearch.php` and returns employee ID, number, last name, first name, and active flag. - `getMitarbeiter(suchstring?, mitarbeitergruppe?)` reads `/module/mitarbeiter/livesearch.php` and returns employee ID, number, last name, first name, and active flag.
- `getTaetigkeiten()` reads `/module/arbeitszeit/taetigkeiten-livesearch.php` (active and inactive) and returns activity ID, UUID, number, name, active flag, area (`bereichId`, `bereich`) and the flags `maschineZuordnen`, `abwesenheit`, `notizErzwingen`.
- Agrarmonitor has no cost center master data (`Kostenstelle` is only a free-text field on machines, employees and articles), so there is no `getKostenstellen()`. See `docs/zeitbuchung-analyse.md`.
- `beginneArbeitstag(mitarbeiterId, start, { datum? })` starts the workday: creates a running shift (attendance without end) for any employee and returns `{ ref }` (`schicht:<mitarbeiterId>:<YYYY-MM-DD>:<id>`). If a running shift already exists that day, its ref is returned instead. The Agrarmonitor menu link "Arbeitstag beginnen" (`bueroAnundAbmelden.php`) only works for the logged-in user, so the connector uses `module/auftraege/api/saveSchicht.php`.
- `beendeArbeitstag(ref, ende)` ends the shift, `updateArbeitstag(ref, { start, ende })` corrects it (`ende: null` keeps it running), `deleteArbeitstag(ref)` deletes the shift including all activities and breaks.
- `createZeitbuchung(daten)` books an activity block (`taetigkeitId`, optional `maschinenId`, `notiz`, `kostenstelle`) or a break (`art: 'pause'`) for `mitarbeiterId` from `start` to `ende` (minute precision, Europe/Berlin) and returns `{ ref }`. Bookings go into the shift of that day (started with `beginneArbeitstag`), which is extended if needed; a running shift stays open. Without any shift, an activity creates one via `nachtragen.php`, a break is rejected. Agrarmonitor has no cost center field, so `kostenstelle` is appended to the note as `Kostenstelle: …`. Pass `datum` (YYYY-MM-DD) for blocks after midnight that belong to the previous workday.
- `updateZeitbuchung(ref, daten)` changes times, activity, machine and note of a booking (the old note is replaced); changing the type (activity/break), employee or workday is rejected.
- `deleteZeitbuchung(ref)` deletes a booking; deleting the last activity of a shift also deletes the shift in Agrarmonitor.
- Refs look like `verteilung:<mitarbeiterId>:<YYYY-MM-DD>:<id>` or `pause:…`. Rejections by Agrarmonitor (HTTP 4xx such as 422 with the reason in the body) and invalid input throw `ZeitbuchungError` (`grund`, `status`); network and 5xx errors are thrown unchanged so callers can retry.
- `saveSession()` explicitly persists the current cookie jar. - `saveSession()` explicitly persists the current cookie jar.
+100 -1
View File
@@ -1,8 +1,10 @@
import { type AxiosInstance } from 'axios'; import { type AxiosInstance } from 'axios';
import type { AgrarmonitorApiCustomer, AgrarmonitorConnectorOptions, AgrarmonitorConnectorResult, AgrarmonitorDeviceRegistrationOptions, AgrarmonitorDeviceRegistrationResult, AgrarmonitorFetchCustomersOptions, AgrarmonitorFreischaltungStatus, AgrarmonitorKunde2, AgrarmonitorRegistrierungStatus, Artikel, ArtikelEinheit, ArtikelKategorie, EingangsrechnungLivesearchResult, Firma, Maschine, MaschinenKategorie, Mitarbeiter, Rechnungsdaten } from './types'; import type { AgrarmonitorApiCustomer, AgrarmonitorConnectorOptions, AgrarmonitorConnectorResult, AgrarmonitorDeviceRegistrationOptions, AgrarmonitorDeviceRegistrationResult, AgrarmonitorFetchCustomersOptions, AgrarmonitorFreischaltungStatus, AgrarmonitorKunde2, AgrarmonitorRegistrierungStatus, Artikel, ArtikelEinheit, ArtikelKategorie, EingangsrechnungLivesearchResult, Firma, Maschine, MaschineMitGruppe, MaschinenKategorie, Mitarbeiter, Rechnungsdaten, Taetigkeit, ZeitbuchungDaten } from './types';
export declare class AgrarmonitorConnector implements AgrarmonitorConnectorResult { export declare class AgrarmonitorConnector implements AgrarmonitorConnectorResult {
private readonly options; private readonly options;
http: AxiosInstance; http: AxiosInstance;
private static readonly maxLivesearchSeiten;
private static readonly berlinFormat;
private static readonly s3DateienBaseUrl; private static readonly s3DateienBaseUrl;
private readonly baseUrl; private readonly baseUrl;
private readonly apiBaseUrl; private readonly apiBaseUrl;
@@ -36,11 +38,108 @@ export declare class AgrarmonitorConnector implements AgrarmonitorConnectorResul
getCustomerById(id: number): Promise<AgrarmonitorApiCustomer>; getCustomerById(id: number): Promise<AgrarmonitorApiCustomer>;
getMaschinenKategorien(): Promise<MaschinenKategorie[]>; getMaschinenKategorien(): Promise<MaschinenKategorie[]>;
getMaschinen(gruppenId: number, suchstring?: string): Promise<Maschine[]>; getMaschinen(gruppenId: number, suchstring?: string): Promise<Maschine[]>;
getAlleMaschinen(): Promise<MaschineMitGruppe[]>;
getFirmen(): Promise<Firma[]>; getFirmen(): Promise<Firma[]>;
getArtikelEinheiten(): Promise<ArtikelEinheit[]>; getArtikelEinheiten(): Promise<ArtikelEinheit[]>;
getArtikelKategorien(): Promise<ArtikelKategorie[]>; getArtikelKategorien(): Promise<ArtikelKategorie[]>;
getArtikel(artikelGruppe: number, suchstring?: string): Promise<Artikel[]>; getArtikel(artikelGruppe: number, suchstring?: string): Promise<Artikel[]>;
getMitarbeiter(suchstring?: string, mitarbeitergruppe?: number): Promise<Mitarbeiter[]>; getMitarbeiter(suchstring?: string, mitarbeitergruppe?: number): Promise<Mitarbeiter[]>;
getTaetigkeiten(): Promise<Taetigkeit[]>;
/**
* Beginnt den Arbeitstag: legt eine laufende Schicht (Anwesenheit ohne Ende)
* an, wie es das Agrarmonitor-Menue "Arbeitstag beginnen" tut - aber fuer
* beliebige Mitarbeiter (saveSchicht.php statt bueroAnundAbmelden.php, das nur
* fuer den angemeldeten Benutzer wirkt). Laeuft am Tag schon eine Schicht,
* wird deren Referenz geliefert, damit Wiederholungen nichts doppelt anlegen.
*/
beginneArbeitstag(mitarbeiterId: number, start: Date, options?: {
datum?: string;
}): Promise<{
ref: string;
}>;
beendeArbeitstag(ref: string, ende: Date): Promise<void>;
/** Korrigiert Beginn und Ende; `ende: null` laesst den Arbeitstag weiterlaufen. */
updateArbeitstag(ref: string, zeiten: {
start: Date;
ende: Date | null;
}): Promise<void>;
/** Loescht die Schicht samt aller Taetigkeiten und Pausen darin. */
deleteArbeitstag(ref: string): Promise<void>;
private arbeitstagZeitpunkt;
private arbeitstagDatum;
private arbeitstagRef;
private parseArbeitstagRef;
private ladeArbeitstag;
private arbeitstagSetzen;
/**
* Bucht einen Taetigkeitsblock oder eine Pause. Gibt es fuer Mitarbeiter und
* Arbeitstag noch keine Schicht, legt `nachtragen.php` Schicht und Verteilung
* an (Pausen brauchen eine vorhandene Schicht). Sonst kommt die Buchung in die
* passende Schicht, die bei Bedarf verlaengert wird.
*/
createZeitbuchung(daten: ZeitbuchungDaten): Promise<{
ref: string;
}>;
updateZeitbuchung(ref: string, daten: ZeitbuchungDaten): Promise<void>;
deleteZeitbuchung(ref: string): Promise<void>;
private zeitbuchungAufbereiten;
/** Rundet auf die volle Minute - Agrarmonitor speichert keine Sekunden. */
private zeitpunkt;
/** YYYY-MM-DD HH:MM -> dd.mm.yy (HH:MM) */
private vergleichZuFormular;
private zeitbuchungRef;
private parseZeitbuchungRef;
private ladeTagesansicht;
private ladeSchichten;
private ladeSchicht;
/** Schicht, die sich mit der Buchung ueberschneidet, sonst die letzte davor, sonst die erste. */
private passendeSchicht;
/**
* Neue Verteilungen verlaengern die Schicht in Agrarmonitor nicht von selbst.
* Eine laufende Schicht bleibt offen - nur ihr Start wird bei Bedarf vorgezogen.
*/
private schichtErweitern;
private schichtSetzen;
/**
* Erste Buchung des Tages: nachtragen.php legt Schicht und Verteilung an,
* liefert aber keine ID. Die Verteilung wird danach in der Tagesansicht gesucht.
*/
private taetigkeitNachtragen;
/**
* editVerteilungForm.php mit shiftID legt sofort eine leere Verteilung an
* (das Agrarmonitor-UI loescht sie bei "Abbrechen"). Schlaegt das Speichern
* fehl, wird dieser Platzhalter wieder entfernt.
*/
private verteilungAnlegen;
private ladeVerteilungFormular;
private verteilungSpeichern;
/** Wie bei Verteilungen legt editPauseForm.php mit shiftID sofort eine Pause an. */
private pauseAnlegen;
private pauseSpeichern;
/**
* JSON-POST an die Arbeitszeit-API. 4xx (z. B. 422 mit Grund im Body) wird
* als ZeitbuchungError geworfen; Netzwerk- und 5xx-Fehler bleiben Axios-Fehler.
*/
private zeitbuchungPost;
private zeitbuchungAntwortPruefen;
/** 422-Antworten sehen so aus: {"verteilungForm":"ganzertag must be present"} */
private zeitbuchungGrund;
/**
* Agrarmonitor speichert auch unbekannte oder inaktive Taetigkeits-IDs (dann
* ohne Taetigkeit). Deshalb gegen die Auswahl im Buchungsformular pruefen.
*/
private taetigkeitPruefen;
private selectedValue;
private normalisiereText;
/**
* Laedt eine Livesearch-Liste Seite fuer Seite. Die Seitenzahl steht in
* `.pagination[data-pages]`; zusaetzlich bricht die Schleife ab, sobald eine
* Seite keine neuen Zeilen bringt (Agrarmonitor ignoriert `seite` teils).
*/
private loadLivesearchSeiten;
private vorherigeUeberschrift;
/** Agrarmonitor zeigt leere Werte in Livesearch-Tabellen als "-" an. */
private livesearchText;
private createHttpClient; private createHttpClient;
private createApiHttpClient; private createApiHttpClient;
private apiRequest; private apiRequest;
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"AgrarmonitorConnector.d.ts","sourceRoot":"","sources":["../src/AgrarmonitorConnector.ts"],"names":[],"mappings":"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"} {"version":3,"file":"AgrarmonitorConnector.d.ts","sourceRoot":"","sources":["../src/AgrarmonitorConnector.ts"],"names":[],"mappings":"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 truncated
+634 -2
View File
@@ -8,9 +8,20 @@ const axios_1 = __importDefault(require("axios"));
const axios_cookiejar_support_1 = require("axios-cookiejar-support"); const axios_cookiejar_support_1 = require("axios-cookiejar-support");
const jsdom_1 = require("jsdom"); const jsdom_1 = require("jsdom");
const tough_cookie_1 = require("tough-cookie"); const tough_cookie_1 = require("tough-cookie");
const ZeitbuchungError_1 = require("./ZeitbuchungError");
class AgrarmonitorConnector { class AgrarmonitorConnector {
options; options;
http; http;
static maxLivesearchSeiten = 500;
static berlinFormat = new Intl.DateTimeFormat('de-DE', {
timeZone: 'Europe/Berlin',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hourCycle: 'h23',
});
static s3DateienBaseUrl = 'https://s3-eu-central-1.amazonaws.com/dateien.agrarmonitor.de/07'; static s3DateienBaseUrl = 'https://s3-eu-central-1.amazonaws.com/dateien.agrarmonitor.de/07';
baseUrl; baseUrl;
apiBaseUrl; apiBaseUrl;
@@ -324,6 +335,38 @@ class AgrarmonitorConnector {
}; };
}); });
} }
async getAlleMaschinen() {
const documents = await this.loadLivesearchSeiten('/module/maschinen/livesearch.php', { suchstring: '', maschinen_aktiv: 1, maschinengruppe: 0 }, 'table.maschinen_table tbody tr[data-id]');
const maschinen = new Map();
for (const document of documents) {
for (const table of Array.from(document.querySelectorAll('table.maschinen_table'))) {
const gruppenId = this.parseNumber(table.id.replace('maschinen_table_', ''));
const gruppe = this.vorherigeUeberschrift(table)?.textContent?.trim() ?? '';
for (const row of Array.from(table.querySelectorAll('tbody tr[data-id]'))) {
const id = this.parseNumber(row.getAttribute('data-id'));
const vorhanden = maschinen.get(id);
if (vorhanden) {
if (!vorhanden.gruppenIds.includes(gruppenId)) {
vorhanden.gruppenIds.push(gruppenId);
}
continue;
}
const cells = Array.from(row.querySelectorAll('td'));
maschinen.set(id, {
id,
nummer: this.livesearchText(cells[0]),
name: this.livesearchText(cells[1]),
kennzeichen: this.livesearchText(cells[2]),
aktiv: Boolean(cells[4]?.querySelector('.fa-check, .text-green')),
gruppenId,
gruppe,
gruppenIds: [gruppenId],
});
}
}
}
return Array.from(maschinen.values());
}
async getFirmen() { async getFirmen() {
const response = await this.http.get('/einstellungen'); const response = await this.http.get('/einstellungen');
await this.saveSession(); await this.saveSession();
@@ -412,6 +455,589 @@ class AgrarmonitorConnector {
}; };
}); });
} }
async getTaetigkeiten() {
const documents = await this.loadLivesearchSeiten('/module/arbeitszeit/taetigkeiten-livesearch.php', { taetigkeiten_aktiv: 1 }, 'table.taetigkeit_table tbody tr[data-id]');
const taetigkeiten = new Map();
for (const document of documents) {
for (const table of Array.from(document.querySelectorAll('table.taetigkeit_table'))) {
const bereichElement = this.vorherigeUeberschrift(table)?.querySelector('.bereich');
for (const row of Array.from(table.querySelectorAll('tbody tr[data-id]'))) {
const id = this.parseNumber(row.getAttribute('data-id'));
const cells = Array.from(row.querySelectorAll('td'));
if (!id || taetigkeiten.has(id)) {
continue;
}
taetigkeiten.set(id, {
id,
uuid: row.getAttribute('data-activityuuid') ?? '',
nummer: cells[0]?.textContent?.trim() ?? '',
name: cells[1]?.textContent?.trim() ?? '',
aktiv: Boolean(row.querySelector('td.toggle .fa-check, td.toggle .text-green')),
bereichId: this.parseNumber(bereichElement?.getAttribute('data-id')),
bereich: bereichElement?.textContent?.trim() ?? '',
maschineZuordnen: this.parseJaNein(cells[2]?.textContent ?? ''),
abwesenheit: this.parseJaNein(cells[3]?.textContent ?? ''),
notizErzwingen: this.parseJaNein(cells[5]?.textContent ?? ''),
});
}
}
}
return Array.from(taetigkeiten.values());
}
/**
* Beginnt den Arbeitstag: legt eine laufende Schicht (Anwesenheit ohne Ende)
* an, wie es das Agrarmonitor-Menue "Arbeitstag beginnen" tut - aber fuer
* beliebige Mitarbeiter (saveSchicht.php statt bueroAnundAbmelden.php, das nur
* fuer den angemeldeten Benutzer wirkt). Laeuft am Tag schon eine Schicht,
* wird deren Referenz geliefert, damit Wiederholungen nichts doppelt anlegen.
*/
async beginneArbeitstag(mitarbeiterId, start, options = {}) {
const beginn = this.arbeitstagZeitpunkt(mitarbeiterId, start, 'start');
const datum = this.arbeitstagDatum(options.datum, beginn);
const laufend = (await this.ladeSchichten(mitarbeiterId, datum)).find(schicht => schicht.ende === null);
if (laufend) {
this.logDebug('Arbeitstag laeuft bereits', { mitarbeiterId, datum, schichtId: laufend.id });
return { ref: this.arbeitstagRef(mitarbeiterId, datum, laufend.id) };
}
const antwort = await this.zeitbuchungPost('/module/auftraege/api/saveSchicht.php', {
vorgang_id: '-1',
mitarbeiter_id: mitarbeiterId,
formdata: { timestamp_start: beginn.formular, timestamp_ende: '' },
}, 'Arbeitstag beginnen');
const id = this.parseNumber(antwort);
if (!id) {
throw new ZeitbuchungError_1.ZeitbuchungError('Agrarmonitor hat keine Schicht angelegt', this.zeitbuchungGrund(antwort));
}
return { ref: this.arbeitstagRef(mitarbeiterId, datum, id) };
}
async beendeArbeitstag(ref, ende) {
const zielRef = this.parseArbeitstagRef(ref);
const schicht = await this.ladeArbeitstag(zielRef, ref);
await this.arbeitstagSetzen(schicht, schicht.start, this.arbeitstagZeitpunkt(zielRef.mitarbeiterId, ende, 'ende').vergleich);
}
/** Korrigiert Beginn und Ende; `ende: null` laesst den Arbeitstag weiterlaufen. */
async updateArbeitstag(ref, zeiten) {
const zielRef = this.parseArbeitstagRef(ref);
const schicht = await this.ladeArbeitstag(zielRef, ref);
const start = this.arbeitstagZeitpunkt(zielRef.mitarbeiterId, zeiten.start, 'start').vergleich;
const ende = zeiten.ende === null ? null : this.arbeitstagZeitpunkt(zielRef.mitarbeiterId, zeiten.ende, 'ende').vergleich;
await this.arbeitstagSetzen(schicht, start, ende);
}
/** Loescht die Schicht samt aller Taetigkeiten und Pausen darin. */
async deleteArbeitstag(ref) {
const zielRef = this.parseArbeitstagRef(ref);
await this.zeitbuchungPost('/module/arbeitszeit/api/deleteSchicht.php', { shiftID: zielRef.id, employeeID: zielRef.mitarbeiterId, date: zielRef.datum }, 'Arbeitstag loeschen');
}
arbeitstagZeitpunkt(mitarbeiterId, date, feld) {
if (!Number.isInteger(mitarbeiterId) || mitarbeiterId <= 0) {
throw new ZeitbuchungError_1.ZeitbuchungError('Ungueltige Mitarbeiter-ID', 'mitarbeiterId');
}
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
throw new ZeitbuchungError_1.ZeitbuchungError(feld === 'start' ? 'Ungueltiger Start' : 'Ungueltiges Ende', feld);
}
return this.zeitpunkt(date);
}
arbeitstagDatum(datum, start) {
if (datum !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(datum)) {
throw new ZeitbuchungError_1.ZeitbuchungError('Ungueltiger Arbeitstag, erwartet YYYY-MM-DD', 'datum');
}
return datum ?? start.datum;
}
arbeitstagRef(mitarbeiterId, datum, schichtId) {
return `schicht:${mitarbeiterId}:${datum}:${schichtId}`;
}
parseArbeitstagRef(ref) {
const match = /^schicht:(\d+):(\d{4}-\d{2}-\d{2}):(\d+)$/.exec(ref);
if (!match) {
throw new ZeitbuchungError_1.ZeitbuchungError(`Ungueltige Arbeitstag-Referenz: ${ref}`, 'ref');
}
return { mitarbeiterId: Number(match[1]), datum: match[2], id: Number(match[3]) };
}
async ladeArbeitstag(zielRef, ref) {
const schicht = await this.ladeSchicht(zielRef.id);
if (!schicht) {
throw new ZeitbuchungError_1.ZeitbuchungError(`Arbeitstag ${ref} nicht gefunden`, 'nicht gefunden');
}
return schicht;
}
async arbeitstagSetzen(schicht, start, ende) {
if (ende !== null && ende <= start) {
throw new ZeitbuchungError_1.ZeitbuchungError('Ende muss mindestens eine Minute nach dem Beginn liegen', 'dauer');
}
await this.schichtSetzen({ id: schicht.id, start, ende });
}
/**
* Bucht einen Taetigkeitsblock oder eine Pause. Gibt es fuer Mitarbeiter und
* Arbeitstag noch keine Schicht, legt `nachtragen.php` Schicht und Verteilung
* an (Pausen brauchen eine vorhandene Schicht). Sonst kommt die Buchung in die
* passende Schicht, die bei Bedarf verlaengert wird.
*/
async createZeitbuchung(daten) {
const buchung = this.zeitbuchungAufbereiten(daten);
const schichten = await this.ladeSchichten(buchung.mitarbeiterId, buchung.datum);
if (schichten.length === 0) {
if (buchung.art === 'pause') {
throw new ZeitbuchungError_1.ZeitbuchungError(`Pause am ${buchung.datum} kann nicht gebucht werden: keine Schicht vorhanden`, 'keine Schicht vorhanden');
}
const id = await this.taetigkeitNachtragen(buchung);
return { ref: this.zeitbuchungRef(buchung, id) };
}
const schicht = this.passendeSchicht(schichten, buchung);
const schichtVorher = { ...schicht };
await this.schichtErweitern(schicht, buchung);
try {
const id = buchung.art === 'pause'
? await this.pauseAnlegen(buchung, schicht.id)
: await this.verteilungAnlegen(buchung, schicht.id);
return { ref: this.zeitbuchungRef(buchung, id) };
}
catch (error) {
if (schicht.start !== schichtVorher.start || schicht.ende !== schichtVorher.ende) {
await this.schichtSetzen(schichtVorher).catch(restoreError => {
this.logger?.warn?.('Schicht konnte nicht zurueckgesetzt werden', {
schichtId: schichtVorher.id,
fehler: restoreError instanceof Error ? restoreError.message : String(restoreError),
});
});
}
throw error;
}
}
async updateZeitbuchung(ref, daten) {
const zielRef = this.parseZeitbuchungRef(ref);
const buchung = this.zeitbuchungAufbereiten(daten);
if (buchung.art !== zielRef.art) {
throw new ZeitbuchungError_1.ZeitbuchungError('Art der Buchung (Taetigkeit/Pause) kann nicht geaendert werden', 'art geaendert');
}
if (buchung.mitarbeiterId !== zielRef.mitarbeiterId || buchung.datum !== zielRef.datum) {
throw new ZeitbuchungError_1.ZeitbuchungError('Mitarbeiter oder Arbeitstag einer Buchung kann nicht geaendert werden', 'mitarbeiter oder datum geaendert');
}
const tagesansicht = await this.ladeTagesansicht(zielRef.mitarbeiterId, zielRef.datum);
const zeilenKlasse = zielRef.art === 'pause' ? 'pause_betrieb' : 'betrieb';
const schichtId = this.parseNumber(tagesansicht
.querySelector(`tr.${zeilenKlasse}[id="${zielRef.id}"]`)
?.closest('table.verteilung_table')
?.getAttribute('data-schicht'));
if (!schichtId) {
throw new ZeitbuchungError_1.ZeitbuchungError(`Buchung ${ref} nicht gefunden`, 'nicht gefunden');
}
const schicht = await this.ladeSchicht(schichtId);
if (schicht) {
await this.schichtErweitern(schicht, buchung);
}
if (buchung.art === 'pause') {
await this.pauseSpeichern(buchung, zielRef.id, 0);
return;
}
const formular = await this.ladeVerteilungFormular({
employeeID: zielRef.mitarbeiterId,
date: zielRef.datum,
distributionID: String(zielRef.id),
});
this.taetigkeitPruefen(formular, buchung.taetigkeitId);
const notizen = Array.from(formular.querySelectorAll('#notizen_table tbody tr[data-id]'))
.map(row => ({
id: this.parseNumber(row.getAttribute('data-id')),
text: this.normalisiereText(row.querySelectorAll('td')[2]?.textContent ?? ''),
}))
.filter(notiz => notiz.id > 0);
const notizUnveraendert = (notizen.length === 0 && !buchung.notiz) ||
(notizen.length === 1 && notizen[0].text === this.normalisiereText(buchung.notiz));
await this.verteilungSpeichern(buchung, zielRef.id, {
alteTaetigkeitId: this.selectedValue(formular, '#taetigkeit_id') || '0',
projektId: this.selectedValue(formular, '#projekt_id') || '0',
verfasserId: formular.querySelector('#verfasser_id')?.value ?? '',
notiz: notizUnveraendert ? '' : buchung.notiz,
});
// Alte Notizen erst nach erfolgreichem Speichern entfernen.
if (!notizUnveraendert) {
for (const notiz of notizen) {
await this.zeitbuchungPost('/module/notiz/api/delete.php', { noteID: notiz.id }, 'Notiz loeschen');
}
}
}
async deleteZeitbuchung(ref) {
const zielRef = this.parseZeitbuchungRef(ref);
if (zielRef.art === 'pause') {
await this.zeitbuchungPost('/module/arbeitszeit/api/deletePause.php', { pauseID: zielRef.id, employeeID: zielRef.mitarbeiterId, date: zielRef.datum }, 'Pause loeschen');
return;
}
await this.zeitbuchungPost('/module/arbeitszeit/api/deleteVerteilung.php', { distributionID: zielRef.id, employeeID: zielRef.mitarbeiterId, date: zielRef.datum }, 'Buchung loeschen');
}
zeitbuchungAufbereiten(daten) {
if (!Number.isInteger(daten.mitarbeiterId) || daten.mitarbeiterId <= 0) {
throw new ZeitbuchungError_1.ZeitbuchungError('Ungueltige Mitarbeiter-ID', 'mitarbeiterId');
}
if (!(daten.start instanceof Date) || Number.isNaN(daten.start.getTime())) {
throw new ZeitbuchungError_1.ZeitbuchungError('Ungueltiger Start', 'start');
}
if (!(daten.ende instanceof Date) || Number.isNaN(daten.ende.getTime())) {
throw new ZeitbuchungError_1.ZeitbuchungError('Ungueltiges Ende', 'ende');
}
if (daten.datum !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(daten.datum)) {
throw new ZeitbuchungError_1.ZeitbuchungError('Ungueltiger Arbeitstag, erwartet YYYY-MM-DD', 'datum');
}
const start = this.zeitpunkt(daten.start);
const ende = this.zeitpunkt(daten.ende);
if (ende.vergleich <= start.vergleich) {
throw new ZeitbuchungError_1.ZeitbuchungError('Ende muss mindestens eine Minute nach dem Start liegen', 'dauer');
}
const basis = {
mitarbeiterId: daten.mitarbeiterId,
datum: daten.datum ?? start.datum,
start,
ende,
};
if (daten.art === 'pause') {
return { ...basis, art: 'pause', taetigkeitId: 0, maschinenId: 0, notiz: '' };
}
if (!Number.isInteger(daten.taetigkeitId) || daten.taetigkeitId <= 0) {
throw new ZeitbuchungError_1.ZeitbuchungError('Ungueltige Taetigkeits-ID', 'taetigkeitId');
}
const maschinenId = daten.maschinenId ?? 0;
if (!Number.isInteger(maschinenId) || maschinenId < 0) {
throw new ZeitbuchungError_1.ZeitbuchungError('Ungueltige Maschinen-ID', 'maschinenId');
}
const kostenstelle = daten.kostenstelle?.trim();
const notiz = [daten.notiz?.trim(), kostenstelle ? `Kostenstelle: ${kostenstelle}` : '']
.filter(Boolean)
.join('\n');
return { ...basis, art: 'taetigkeit', taetigkeitId: daten.taetigkeitId, maschinenId, notiz };
}
/** Rundet auf die volle Minute - Agrarmonitor speichert keine Sekunden. */
zeitpunkt(date) {
const gerundet = new Date(Math.round(date.getTime() / 60000) * 60000);
const teile = Object.fromEntries(AgrarmonitorConnector.berlinFormat.formatToParts(gerundet).map(teil => [teil.type, teil.value]));
const datum = `${teile.year}-${teile.month}-${teile.day}`;
const uhrzeit = `${teile.hour}:${teile.minute}`;
return {
vergleich: `${datum} ${uhrzeit}`,
formular: this.vergleichZuFormular(`${datum} ${uhrzeit}`),
datum,
uhrzeit,
};
}
/** YYYY-MM-DD HH:MM -> dd.mm.yy (HH:MM) */
vergleichZuFormular(wert) {
const [datum, uhrzeit] = wert.split(' ');
const [jahr, monat, tag] = datum.split('-');
return `${tag}.${monat}.${jahr.slice(2)} (${uhrzeit})`;
}
zeitbuchungRef(buchung, id) {
return `${buchung.art === 'pause' ? 'pause' : 'verteilung'}:${buchung.mitarbeiterId}:${buchung.datum}:${id}`;
}
parseZeitbuchungRef(ref) {
const match = /^(verteilung|pause):(\d+):(\d{4}-\d{2}-\d{2}):(\d+)$/.exec(ref);
if (!match) {
throw new ZeitbuchungError_1.ZeitbuchungError(`Ungueltige Zeitbuchungs-Referenz: ${ref}`, 'ref');
}
return {
art: match[1] === 'pause' ? 'pause' : 'taetigkeit',
mitarbeiterId: Number(match[2]),
datum: match[3],
id: Number(match[4]),
};
}
async ladeTagesansicht(mitarbeiterId, datum) {
const response = await this.http.get(`/arbeitszeit/detail/${mitarbeiterId}/${datum}`);
await this.saveSession();
return this.parseHtmlDocument(response.data);
}
async ladeSchichten(mitarbeiterId, datum) {
const tagesansicht = await this.ladeTagesansicht(mitarbeiterId, datum);
const ids = new Set(Array.from(tagesansicht.querySelectorAll('table.verteilung_table[data-schicht]'))
.map(table => this.parseNumber(table.getAttribute('data-schicht')))
.filter(id => id > 0));
const schichten = [];
for (const id of ids) {
const schicht = await this.ladeSchicht(id);
if (schicht) {
schichten.push(schicht);
}
}
return schichten;
}
async ladeSchicht(schichtId) {
const response = await this.http.get('/module/arbeitszeit/api/getSchicht.php', {
params: { schicht_id: schichtId },
});
await this.saveSession();
const data = (typeof response.data === 'string' ? JSON.parse(response.data || 'null') : response.data);
if (!data?.timestamp_start || Number(data.ist_deleted) === 1) {
return null;
}
const start = data.timestamp_start.slice(0, 16);
const ende = data.timestamp_ende && !data.timestamp_ende.startsWith('0000') ? data.timestamp_ende.slice(0, 16) : null;
return { id: schichtId, start, ende };
}
/** Schicht, die sich mit der Buchung ueberschneidet, sonst die letzte davor, sonst die erste. */
passendeSchicht(schichten, buchung) {
return (schichten.find(schicht => schicht.start <= buchung.ende.vergleich && (schicht.ende === null || buchung.start.vergleich <= schicht.ende)) ??
schichten.filter(schicht => schicht.start <= buchung.start.vergleich).at(-1) ??
schichten[0]);
}
/**
* Neue Verteilungen verlaengern die Schicht in Agrarmonitor nicht von selbst.
* Eine laufende Schicht bleibt offen - nur ihr Start wird bei Bedarf vorgezogen.
*/
async schichtErweitern(schicht, buchung) {
const start = buchung.start.vergleich < schicht.start ? buchung.start.vergleich : schicht.start;
const ende = schicht.ende === null || buchung.ende.vergleich <= schicht.ende ? schicht.ende : buchung.ende.vergleich;
if (start === schicht.start && ende === schicht.ende) {
return;
}
await this.schichtSetzen({ id: schicht.id, start, ende });
schicht.start = start;
schicht.ende = ende;
}
async schichtSetzen(schicht) {
await this.zeitbuchungPost('/module/arbeitszeit/api/updateSchicht.php', {
shiftID: schicht.id,
timestampStart: this.vergleichZuFormular(schicht.start),
// Leeres Ende laesst die Schicht laufen.
timestampEnd: schicht.ende === null ? '' : this.vergleichZuFormular(schicht.ende),
}, 'Schicht anpassen');
}
/**
* Erste Buchung des Tages: nachtragen.php legt Schicht und Verteilung an,
* liefert aber keine ID. Die Verteilung wird danach in der Tagesansicht gesucht.
*/
async taetigkeitNachtragen(buchung) {
const nachtragenFormular = await this.http.get('/module/arbeitszeit/api/nachtragen-form.php');
await this.saveSession();
this.taetigkeitPruefen(this.parseHtmlDocument(nachtragenFormular.data), buchung.taetigkeitId);
const amArbeitstag = buchung.start.datum === buchung.datum && buchung.ende.datum === buchung.datum;
const form = {
taetigkeit_id: String(buchung.taetigkeitId),
projekt_id: '0',
arbeitszeit_start: buchung.start.uhrzeit,
// Nachtragen kennt nur Uhrzeiten; ueber Mitternacht wird unten korrigiert.
arbeitszeit_ende: amArbeitstag ? buchung.ende.uhrzeit : '23:59',
arbeitzeit_sollstunden: '',
pause_start: '',
pause_ende: '',
maschine_id: String(buchung.maschinenId),
notiz: buchung.notiz,
uebernehmen: '1',
mitarbeitertyp: '1',
mitarbeiter_id: String(buchung.mitarbeiterId),
kategorie_id: '0',
};
const body = new URLSearchParams();
for (const [name, value] of Object.entries(form)) {
body.append(`form[${name}]`, value);
}
body.append('tage[]', buchung.datum);
body.append('anzahl_datum', '1');
body.append('planungAusMitarbeiterId', '0');
const response = await this.http.post('/module/arbeitszeit/api/nachtragen.php', body, {
...this.formPostConfig('/arbeitszeit'),
validateStatus: status => status >= 200 && status < 500,
});
await this.saveSession();
this.zeitbuchungAntwortPruefen(response, 'Nachtragen');
const tagesansicht = await this.ladeTagesansicht(buchung.mitarbeiterId, buchung.datum);
const zeilen = Array.from(tagesansicht.querySelectorAll('table.verteilung_table tr.betrieb[id]'));
if (zeilen.length !== 1) {
throw new ZeitbuchungError_1.ZeitbuchungError(`Nachgetragene Buchung am ${buchung.datum} nicht eindeutig gefunden (${zeilen.length} Buchungen)`, 'nicht eindeutig');
}
const id = this.parseNumber(zeilen[0].id);
if (!amArbeitstag) {
const formular = await this.ladeVerteilungFormular({
employeeID: buchung.mitarbeiterId,
date: buchung.datum,
distributionID: String(id),
});
await this.verteilungSpeichern(buchung, id, {
alteTaetigkeitId: String(buchung.taetigkeitId),
projektId: '0',
verfasserId: formular.querySelector('#verfasser_id')?.value ?? '',
notiz: '',
});
const schicht = await this.ladeSchicht(this.parseNumber(zeilen[0].closest('table.verteilung_table')?.getAttribute('data-schicht')));
if (schicht) {
await this.schichtErweitern(schicht, buchung);
}
}
return id;
}
/**
* editVerteilungForm.php mit shiftID legt sofort eine leere Verteilung an
* (das Agrarmonitor-UI loescht sie bei "Abbrechen"). Schlaegt das Speichern
* fehl, wird dieser Platzhalter wieder entfernt.
*/
async verteilungAnlegen(buchung, schichtId) {
const formular = await this.ladeVerteilungFormular({
employeeID: buchung.mitarbeiterId,
date: buchung.datum,
shiftID: schichtId,
});
const id = this.parseNumber(formular.querySelector('#verteilung_form')?.getAttribute('data-id'));
if (!id) {
throw new ZeitbuchungError_1.ZeitbuchungError('Agrarmonitor hat keine neue Buchung angelegt', 'keine verteilung_id');
}
try {
this.taetigkeitPruefen(formular, buchung.taetigkeitId);
await this.verteilungSpeichern(buchung, id, {
alteTaetigkeitId: '0',
projektId: '0',
verfasserId: formular.querySelector('#verfasser_id')?.value ?? '',
notiz: buchung.notiz,
});
}
catch (error) {
await this.zeitbuchungPost('/module/arbeitszeit/api/deleteVerteilung.php', { distributionID: id, employeeID: buchung.mitarbeiterId, date: buchung.datum }, 'Platzhalter loeschen').catch(() => undefined);
throw error;
}
return id;
}
async ladeVerteilungFormular(body) {
const html = await this.zeitbuchungPost('/module/arbeitszeit/api/editVerteilungForm.php', body, 'Buchungsformular laden');
const formular = this.parseHtmlDocument(html);
if (!formular.querySelector('#verteilung_form')) {
throw new ZeitbuchungError_1.ZeitbuchungError('Buchungsformular nicht gefunden', 'kein verteilung_form');
}
return formular;
}
async verteilungSpeichern(buchung, id, formular) {
await this.zeitbuchungPost('/module/arbeitszeit/api/updateVerteilung.php', {
mitarbeiterId: buchung.mitarbeiterId,
verteilungId: String(id),
verteilungForm: {
timestamp_start: buchung.start.formular,
timestamp_ende: buchung.ende.formular,
alte_taetigkeit_id: formular.alteTaetigkeitId,
taetigkeit_id: String(buchung.taetigkeitId),
ganzertag: '1',
maschine_id: String(buchung.maschinenId),
projekt_id: formular.projektId,
},
// Nicht leere notizfeld werden als neue Notiz an die Verteilung gehaengt.
notizForm: {
notizfeld: formular.notiz,
verfasser_id: formular.verfasserId,
},
}, 'Buchung speichern');
}
/** Wie bei Verteilungen legt editPauseForm.php mit shiftID sofort eine Pause an. */
async pauseAnlegen(buchung, schichtId) {
const html = await this.zeitbuchungPost('/module/arbeitszeit/api/editPauseForm.php', { employeeID: buchung.mitarbeiterId, date: buchung.datum, shiftID: schichtId }, 'Pausenformular laden');
const id = this.parseNumber(this.parseHtmlDocument(html).querySelector('#pause_form')?.getAttribute('data-id'));
if (!id) {
throw new ZeitbuchungError_1.ZeitbuchungError('Agrarmonitor hat keine neue Pause angelegt', 'keine pause_id');
}
try {
await this.pauseSpeichern(buchung, id, 1);
}
catch (error) {
await this.zeitbuchungPost('/module/arbeitszeit/api/deletePause.php', { pauseID: id, employeeID: buchung.mitarbeiterId, date: buchung.datum }, 'Platzhalter loeschen').catch(() => undefined);
throw error;
}
return id;
}
async pauseSpeichern(buchung, id, neu) {
await this.zeitbuchungPost('/module/arbeitszeit/api/updatePause.php', {
mitarbeiterId: buchung.mitarbeiterId,
pause_id: String(id),
pauseForm: {
pause_start: buchung.start.formular,
pause_ende: buchung.ende.formular,
},
neu,
}, 'Pause speichern');
}
/**
* JSON-POST an die Arbeitszeit-API. 4xx (z. B. 422 mit Grund im Body) wird
* als ZeitbuchungError geworfen; Netzwerk- und 5xx-Fehler bleiben Axios-Fehler.
*/
async zeitbuchungPost(url, body, aktion) {
const response = await this.http.post(url, body, {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json, */*',
'X-Requested-With': 'XMLHttpRequest',
Referer: `${this.baseUrl}/arbeitszeit`,
Origin: this.baseUrl,
},
validateStatus: status => status >= 200 && status < 500,
});
await this.saveSession();
this.zeitbuchungAntwortPruefen(response, aktion);
return response.data;
}
zeitbuchungAntwortPruefen(response, aktion) {
if (response.status >= 200 && response.status < 300) {
return;
}
const grund = this.zeitbuchungGrund(response.data);
throw new ZeitbuchungError_1.ZeitbuchungError(`${aktion} fehlgeschlagen (HTTP ${response.status}): ${grund}`, grund, response.status);
}
/** 422-Antworten sehen so aus: {"verteilungForm":"ganzertag must be present"} */
zeitbuchungGrund(data) {
if (data && typeof data === 'object') {
const texte = Object.values(data)
.flatMap(wert => (wert && typeof wert === 'object' ? Object.values(wert) : [wert]))
.filter((wert) => typeof wert === 'string' && wert.trim() !== '');
return texte.length ? texte.join('; ') : JSON.stringify(data).slice(0, 500);
}
return String(data ?? '').trim().slice(0, 500) || 'keine Antwort';
}
/**
* Agrarmonitor speichert auch unbekannte oder inaktive Taetigkeits-IDs (dann
* ohne Taetigkeit). Deshalb gegen die Auswahl im Buchungsformular pruefen.
*/
taetigkeitPruefen(formular, taetigkeitId) {
const buchbar = Array.from(formular.querySelectorAll('select#taetigkeit_id option')).map(option => option.value);
if (!buchbar.includes(String(taetigkeitId))) {
throw new ZeitbuchungError_1.ZeitbuchungError(`Taetigkeit ${taetigkeitId} ist in Agrarmonitor nicht buchbar (unbekannt oder inaktiv)`, 'taetigkeit nicht buchbar');
}
}
selectedValue(document, selector) {
const select = document.querySelector(selector);
return select?.querySelector('option[selected]')?.value ?? select?.value ?? '';
}
normalisiereText(text) {
return text.replace(/\s+/g, ' ').trim();
}
/**
* Laedt eine Livesearch-Liste Seite fuer Seite. Die Seitenzahl steht in
* `.pagination[data-pages]`; zusaetzlich bricht die Schleife ab, sobald eine
* Seite keine neuen Zeilen bringt (Agrarmonitor ignoriert `seite` teils).
*/
async loadLivesearchSeiten(url, params, rowSelector) {
const documents = [];
const bekannteZeilen = new Set();
for (let seite = 1; seite <= AgrarmonitorConnector.maxLivesearchSeiten; seite += 1) {
const response = await this.http.get(url, { params: { ...params, seite } });
await this.saveSession();
const document = this.parseHtmlDocument(response.data);
const neueZeilen = Array.from(document.querySelectorAll(rowSelector))
.map(row => `${row.closest('table')?.id ?? ''}:${row.getAttribute('data-id') ?? row.id}`)
.filter(key => !bekannteZeilen.has(key));
if (neueZeilen.length === 0) {
break;
}
neueZeilen.forEach(key => bekannteZeilen.add(key));
documents.push(document);
const seiten = this.parseNumber(document.querySelector('.pagination')?.getAttribute('data-pages'));
if (seite >= seiten) {
break;
}
}
return documents;
}
vorherigeUeberschrift(element) {
let sibling = element.previousElementSibling;
while (sibling && !/^H[1-6]$/.test(sibling.tagName)) {
sibling = sibling.previousElementSibling;
}
return sibling;
}
/** Agrarmonitor zeigt leere Werte in Livesearch-Tabellen als "-" an. */
livesearchText(cell) {
const text = cell?.textContent?.trim() ?? '';
return text === '-' ? '' : text;
}
createHttpClient() { createHttpClient() {
const client = (0, axios_cookiejar_support_1.wrapper)(axios_1.default.create({ const client = (0, axios_cookiejar_support_1.wrapper)(axios_1.default.create({
baseURL: this.baseUrl, baseURL: this.baseUrl,
@@ -696,8 +1322,14 @@ class AgrarmonitorConnector {
const document = this.parseHtmlDocument(response.data); const document = this.parseHtmlDocument(response.data);
const receivedStatus = document.querySelector('#receivedStatus'); const receivedStatus = document.querySelector('#receivedStatus');
const receivedText = receivedStatus?.textContent?.trim() ?? ''; const receivedText = receivedStatus?.textContent?.trim() ?? '';
const parentParts = (receivedStatus?.parentElement?.textContent ?? '').split('-'); // Buchungsstatus steht in #bookedStatus. Fallback für das alte Layout: der Teil der
const bookingText = parentParts.at(-1)?.trim() ?? ''; // Statuszeile mit "gebucht" (nicht einfach der letzte, dahinter folgt "Zahlung erfassen").
const bookingText = document.querySelector('#bookedStatus')?.textContent?.trim() ??
(receivedStatus?.parentElement?.textContent ?? '')
.split('-')
.map(part => part.trim())
.find(part => /gebucht/i.test(part)) ??
'';
return { return {
eingangsDatum: !receivedText || receivedText === 'Nicht empfangen' eingangsDatum: !receivedText || receivedText === 'Nicht empfangen'
? null ? null
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"AgrarmonitorConnector.js","sourceRoot":"","sources":["../src/AgrarmonitorConnector.ts"],"names":[],"mappings":"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 truncated {"version":3,"file":"AgrarmonitorConnector.js","sourceRoot":"","sources":["../src/AgrarmonitorConnector.ts"],"names":[],"mappings":"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 truncated
+12
View File
@@ -0,0 +1,12 @@
/**
* Agrarmonitor hat eine Zeitbuchung fachlich abgelehnt (z. B. HTTP 422 mit
* Grund im Body) oder die Buchung ist so nicht moeglich. Ein erneuter Versuch
* mit denselben Daten hilft nicht - anders als bei Netzwerk- oder 5xx-Fehlern,
* die als normale Fehler durchgereicht werden.
*/
export declare class ZeitbuchungError extends Error {
readonly grund: string;
readonly status?: number | undefined;
constructor(message: string, grund: string, status?: number | undefined);
}
//# sourceMappingURL=ZeitbuchungError.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"ZeitbuchungError.d.ts","sourceRoot":"","sources":["../src/ZeitbuchungError.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;aAGvB,KAAK,EAAE,MAAM;aACb,MAAM,CAAC,EAAE,MAAM;gBAF/B,OAAO,EAAE,MAAM,EACC,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,MAAM,YAAA;CAKlC"}
+21
View File
@@ -0,0 +1,21 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ZeitbuchungError = void 0;
/**
* Agrarmonitor hat eine Zeitbuchung fachlich abgelehnt (z. B. HTTP 422 mit
* Grund im Body) oder die Buchung ist so nicht moeglich. Ein erneuter Versuch
* mit denselben Daten hilft nicht - anders als bei Netzwerk- oder 5xx-Fehlern,
* die als normale Fehler durchgereicht werden.
*/
class ZeitbuchungError extends Error {
grund;
status;
constructor(message, grund, status) {
super(message);
this.grund = grund;
this.status = status;
this.name = 'ZeitbuchungError';
}
}
exports.ZeitbuchungError = ZeitbuchungError;
//# sourceMappingURL=ZeitbuchungError.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"ZeitbuchungError.js","sourceRoot":"","sources":["../src/ZeitbuchungError.ts"],"names":[],"mappings":";;;AAAA;;;;;GAKG;AACH,MAAa,gBAAiB,SAAQ,KAAK;IAGvB;IACA;IAHlB,YACE,OAAe,EACC,KAAa,EACb,MAAe;QAE/B,KAAK,CAAC,OAAO,CAAC,CAAC;QAHC,UAAK,GAAL,KAAK,CAAQ;QACb,WAAM,GAAN,MAAM,CAAS;QAG/B,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF;AATD,4CASC"}
+2 -1
View File
@@ -3,5 +3,6 @@ export { FileCookieStore } from './cookie-store/FileCookieStore';
export { MemoryCookieStore } from './cookie-store/MemoryCookieStore'; export { MemoryCookieStore } from './cookie-store/MemoryCookieStore';
export { AgrarmonitorConnector } from './AgrarmonitorConnector'; export { AgrarmonitorConnector } from './AgrarmonitorConnector';
export { createAgrarmonitorClient } from './createAgrarmonitorClient'; export { createAgrarmonitorClient } from './createAgrarmonitorClient';
export type { AgrarmonitorConnectorOptions, AgrarmonitorConnectorResult, CookieEncryptor, CookieStore, Logger, } from './types'; export { ZeitbuchungError } from './ZeitbuchungError';
export type { AgrarmonitorConnectorOptions, AgrarmonitorConnectorResult, CookieEncryptor, CookieStore, Logger, MaschineMitGruppe, PausenbuchungDaten, Taetigkeit, TaetigkeitsbuchungDaten, ZeitbuchungDaten, } from './types';
//# sourceMappingURL=index.d.ts.map //# sourceMappingURL=index.d.ts.map
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AACtE,YAAY,EACV,4BAA4B,EAC5B,2BAA2B,EAC3B,eAAe,EACf,WAAW,EACX,MAAM,GACP,MAAM,SAAS,CAAC"} {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AACtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,YAAY,EACV,4BAA4B,EAC5B,2BAA2B,EAC3B,eAAe,EACf,WAAW,EACX,MAAM,EACN,iBAAiB,EACjB,kBAAkB,EAClB,UAAU,EACV,uBAAuB,EACvB,gBAAgB,GACjB,MAAM,SAAS,CAAC"}
+3 -1
View File
@@ -1,6 +1,6 @@
"use strict"; "use strict";
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.createAgrarmonitorClient = exports.AgrarmonitorConnector = exports.MemoryCookieStore = exports.FileCookieStore = exports.AesGcmCookieEncryptor = void 0; exports.ZeitbuchungError = exports.createAgrarmonitorClient = exports.AgrarmonitorConnector = exports.MemoryCookieStore = exports.FileCookieStore = exports.AesGcmCookieEncryptor = void 0;
var AesGcmCookieEncryptor_1 = require("./crypto/AesGcmCookieEncryptor"); var AesGcmCookieEncryptor_1 = require("./crypto/AesGcmCookieEncryptor");
Object.defineProperty(exports, "AesGcmCookieEncryptor", { enumerable: true, get: function () { return AesGcmCookieEncryptor_1.AesGcmCookieEncryptor; } }); Object.defineProperty(exports, "AesGcmCookieEncryptor", { enumerable: true, get: function () { return AesGcmCookieEncryptor_1.AesGcmCookieEncryptor; } });
var FileCookieStore_1 = require("./cookie-store/FileCookieStore"); var FileCookieStore_1 = require("./cookie-store/FileCookieStore");
@@ -11,4 +11,6 @@ var AgrarmonitorConnector_1 = require("./AgrarmonitorConnector");
Object.defineProperty(exports, "AgrarmonitorConnector", { enumerable: true, get: function () { return AgrarmonitorConnector_1.AgrarmonitorConnector; } }); Object.defineProperty(exports, "AgrarmonitorConnector", { enumerable: true, get: function () { return AgrarmonitorConnector_1.AgrarmonitorConnector; } });
var createAgrarmonitorClient_1 = require("./createAgrarmonitorClient"); var createAgrarmonitorClient_1 = require("./createAgrarmonitorClient");
Object.defineProperty(exports, "createAgrarmonitorClient", { enumerable: true, get: function () { return createAgrarmonitorClient_1.createAgrarmonitorClient; } }); Object.defineProperty(exports, "createAgrarmonitorClient", { enumerable: true, get: function () { return createAgrarmonitorClient_1.createAgrarmonitorClient; } });
var ZeitbuchungError_1 = require("./ZeitbuchungError");
Object.defineProperty(exports, "ZeitbuchungError", { enumerable: true, get: function () { return ZeitbuchungError_1.ZeitbuchungError; } });
//# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,wEAAuE;AAA9D,8HAAA,qBAAqB,OAAA;AAC9B,kEAAiE;AAAxD,kHAAA,eAAe,OAAA;AACxB,sEAAqE;AAA5D,sHAAA,iBAAiB,OAAA;AAC1B,iEAAgE;AAAvD,8HAAA,qBAAqB,OAAA;AAC9B,uEAAsE;AAA7D,oIAAA,wBAAwB,OAAA"} {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,wEAAuE;AAA9D,8HAAA,qBAAqB,OAAA;AAC9B,kEAAiE;AAAxD,kHAAA,eAAe,OAAA;AACxB,sEAAqE;AAA5D,sHAAA,iBAAiB,OAAA;AAC1B,iEAAgE;AAAvD,8HAAA,qBAAqB,OAAA;AAC9B,uEAAsE;AAA7D,oIAAA,wBAAwB,OAAA;AACjC,uDAAsD;AAA7C,oHAAA,gBAAgB,OAAA"}
+65
View File
@@ -50,11 +50,29 @@ export interface AgrarmonitorConnectorResult {
getCustomerById(id: number): Promise<AgrarmonitorApiCustomer>; getCustomerById(id: number): Promise<AgrarmonitorApiCustomer>;
getMaschinenKategorien(): Promise<MaschinenKategorie[]>; getMaschinenKategorien(): Promise<MaschinenKategorie[]>;
getMaschinen(gruppenId: number, suchstring?: string): Promise<Maschine[]>; getMaschinen(gruppenId: number, suchstring?: string): Promise<Maschine[]>;
getAlleMaschinen(): Promise<MaschineMitGruppe[]>;
getFirmen(): Promise<Firma[]>; getFirmen(): Promise<Firma[]>;
getArtikelEinheiten(): Promise<ArtikelEinheit[]>; getArtikelEinheiten(): Promise<ArtikelEinheit[]>;
getArtikelKategorien(): Promise<ArtikelKategorie[]>; getArtikelKategorien(): Promise<ArtikelKategorie[]>;
getArtikel(artikelGruppe: number, suchstring?: string): Promise<Artikel[]>; getArtikel(artikelGruppe: number, suchstring?: string): Promise<Artikel[]>;
getMitarbeiter(suchstring?: string, mitarbeitergruppe?: number): Promise<Mitarbeiter[]>; getMitarbeiter(suchstring?: string, mitarbeitergruppe?: number): Promise<Mitarbeiter[]>;
getTaetigkeiten(): Promise<Taetigkeit[]>;
beginneArbeitstag(mitarbeiterId: number, start: Date, options?: {
datum?: string;
}): Promise<{
ref: string;
}>;
beendeArbeitstag(ref: string, ende: Date): Promise<void>;
updateArbeitstag(ref: string, zeiten: {
start: Date;
ende: Date | null;
}): Promise<void>;
deleteArbeitstag(ref: string): Promise<void>;
createZeitbuchung(daten: ZeitbuchungDaten): Promise<{
ref: string;
}>;
updateZeitbuchung(ref: string, daten: ZeitbuchungDaten): Promise<void>;
deleteZeitbuchung(ref: string): Promise<void>;
} }
export interface AgrarmonitorFreischaltungStatus { export interface AgrarmonitorFreischaltungStatus {
freigeschaltet: boolean; freigeschaltet: boolean;
@@ -152,6 +170,13 @@ export interface Maschine {
kennzeichen: string; kennzeichen: string;
aktiv: boolean; aktiv: boolean;
} }
export interface MaschineMitGruppe extends Maschine {
/** Erste Maschinenkategorie, in der die Maschine gelistet ist */
gruppenId: number;
gruppe: string;
/** Alle Maschinenkategorien - eine Maschine kann in mehreren stehen */
gruppenIds: number[];
}
export interface Firma { export interface Firma {
id: number; id: number;
bezeichnung: string; bezeichnung: string;
@@ -177,4 +202,44 @@ export interface Mitarbeiter {
vorname: string; vorname: string;
aktiv: boolean; aktiv: boolean;
} }
export interface Taetigkeit {
id: number;
uuid: string;
nummer: string;
name: string;
aktiv: boolean;
bereichId: number;
/** Ueberschrift des Bereichs, z. B. "100 Werkstatt (Mobil sichtbar)" */
bereich: string;
/** Spalte "Maschinen zuordnen" */
maschineZuordnen: boolean;
abwesenheit: boolean;
notizErzwingen: boolean;
}
interface ZeitbuchungBasis {
/** Agrarmonitor-Mitarbeiter-ID */
mitarbeiterId: number;
start: Date;
ende: Date;
/**
* Arbeitstag (YYYY-MM-DD), zu dessen Schicht die Buchung gehoert.
* Standard: Datum von `start` in Europe/Berlin. Fuer Bloecke nach
* Mitternacht den Tag des Arbeitsbeginns angeben.
*/
datum?: string;
}
export interface TaetigkeitsbuchungDaten extends ZeitbuchungBasis {
art?: 'taetigkeit';
taetigkeitId: number;
maschinenId?: number;
/** Agrarmonitor hat kein Kostenstellen-Feld - landet als Zeile in der Notiz */
kostenstelle?: string;
notiz?: string;
}
/** Pausen haben in Agrarmonitor keine Notiz. */
export interface PausenbuchungDaten extends ZeitbuchungBasis {
art: 'pause';
}
export type ZeitbuchungDaten = TaetigkeitsbuchungDaten | PausenbuchungDaten;
export {};
//# sourceMappingURL=types.d.ts.map //# sourceMappingURL=types.d.ts.map
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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 truncated {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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 truncated
+146
View File
@@ -0,0 +1,146 @@
# Zeitbuchung in Agrarmonitor – Analyse
Stand: 2026-10-14 · Grundlage: Live-Erkundung in `admin7.agrarmonitor.de` (Web-Oberfläche, eingeloggt im Browser) und das aktuelle Frontend-Bundle `assets/Admin.min.js`. Die Kopie `Fahrzeugübersicht/public/js/main.js` ist veraltet (alte Endpunkte `ajax-v3/arbeitszeit.php?id=…` liefern heute 404).
Schreibende Requests wurden nur für den Testmitarbeiter **3309** abgesetzt (Freigabe durch Björn) und danach vollständig wieder gelöscht.
## 1. Stammdaten
Alle Livesearch-Endpunkte liefern ein HTML-Fragment. Paging: Der generische Livesearch-Client liest `.pagination[data-pages]` aus der Antwort; fehlt das Element oder ist `data-pages` ≤ 1, gibt es nur eine Seite. Bei Tätigkeiten und Maschinen gibt es aktuell kein Paging (alle Zeilen auf Seite 1, `seite=2` liefert dieselben Zeilen).
### Tätigkeiten (Tätigkeitsarten)
- Seite: `/arbeitszeit/taetigkeiten` („Arbeiten auf dem Betrieb verwalten“)
- Livesearch: `GET /module/arbeitszeit/taetigkeiten-livesearch.php?seite=1&taetigkeiten_aktiv=1`
- `taetigkeiten_aktiv`: `0` = nur aktive (Standard der Seite), `1` = aktive und inaktive, `2` = nur inaktive
- aktuell 54 Tätigkeiten (33 aktiv)
- Aufbau: je „Bereich“ eine Überschrift `<h2><b class="bereich" data-id="17">100 Werkstatt (Mobil sichtbar)</b></h2>`, darauf folgend `table.taetigkeit_table`
- Zeile: `tr[data-id][data-activityuuid]`
- `data-id` = stabile Tätigkeits-ID (wird als `taetigkeit_id` gebucht)
- Spalten: 0 Nummer · 1 Bezeichnung · 2 Maschinen zuordnen (Ja/Nein) · 3 Abwesenheit · 4 Als Urlaub werten · 5 Notiz erzwingen · 6 Wartungsplan · 7 Erweiterte Effizienz · 8 Aktiv (`td.toggle` mit `.fa-check` = aktiv, `.fa-close` = inaktiv)
- Bereiche (aktuell): 100 Werkstatt, 200 Auftragsnebenzeiten, 300 Büro, 400 Hof-/Feldarbeit, 500 Gülle Mann solo, 600 LKW, 700 Rüstzeiten, 800 Pause, 900 Abwesenheit
- Hinweis: Bezeichnungen sind in Agrarmonitor teils abgeschnitten gespeichert (z. B. „Arbeiten, die keiner Maschine oder anderen Tätigkeit z“) – identisch in Liste und Buchungsformular.
### Maschinen
- Seite: `/maschinen`
- Livesearch: `GET /module/maschinen/livesearch.php?suchstring=&maschinen_aktiv=1&maschinengruppe=0&seite=1`
- `maschinen_aktiv`: `0` = nur aktive (Standard der Seite), **`1` = aktive und inaktive**, `2` = nur inaktive
- `maschinengruppe`: `0` = alle Kategorien, `-98` = alle Zugmaschinen, `-99` = alle Anbaumaschinen, sonst Kategorie-ID
- Aufbau: je Kategorie eine Überschrift `<h2>Schlepper</h2>` und `table.maschinen_table#maschinen_table_{kategorieId}`
- Zeile: `tr[data-id]`, Spalten: 0 Nummer · 1 Bezeichnung · 2 Kennzeichen · 3 Zähler · 4 Aktiv (`.fa-check`/`.fa-close`). Leere Werte werden als `-` angezeigt.
- **Eine Maschine kann in mehreren Kategorien stehen**: 382 Zeilen, aber nur 263 verschiedene Maschinen (99 Maschinen mehrfach).
- `getMaschinen(gruppenId)` sendet bereits `maschinen_aktiv: 1`, liefert also – anders als angenommen – aktive **und** inaktive Maschinen einer Kategorie. Paging ist für Maschinen nicht relevant (eine Seite).
- Das Buchungsformular bietet nur eine Teilmenge an (147 Maschinen im Verteilungs-Formular, 243 im Nachtragen-Formular). Welche Maschinen dort fehlen (inaktive? nicht mobil sichtbare?), ist nicht geprüft.
### Kostenstellen
- **Es gibt keine Kostenstellen-Stammdaten** (keine Seite, keine Livesearch, kein Auswahlfeld).
- „Kostenstelle“ ist nur ein Freitextfeld (max. 10 Zeichen) an Maschine (`/maschinen/detail/{id}`, Feld `kostenstelle`), Mitarbeiter und Artikel. Es wird beim DATEV-Export verwendet (Einstellung „Kostenstellen exportieren“).
- Das Modul „Betriebszweige“ (`/betriebszweige`) ist in diesem Mandanten nicht freigeschaltet („Diese Seite existiert nicht“).
- Eine Arbeitszeit-Buchung hat **kein Kostenstellen-Feld**. Am nächsten kommt das Feld **Projekt** (`projekt_id`, 16 Projekte unter `/projekte`).
### Mitarbeiter
- `getMitarbeiter()` nutzt `module/mitarbeiter/livesearch.php` mit `mitarbeiter_aktiv=1` (= aktive und inaktive) und `itemsperpage=100000` – reicht für die Zuordnung `am_mitarbeiter_id`.
## 2. Buchungsschnittstelle
**Gefunden: ja** (Web-Oberfläche, JSON-Endpunkte unter `module/arbeitszeit/api/`). Die Token-API `api.agrarmonitor.de/v1` ist nicht öffentlich dokumentiert (Weiterleitung auf `agrarmonitor.de/schnittstellen`, „Sprechen Sie uns an“); der Connector nutzt dort nur `/kunden`.
### Datenmodell in Agrarmonitor
- **Schicht** (Anwesenheit): ein Zeitraum pro Mitarbeiter und Tag (`schicht_id`), mehrere pro Tag möglich.
- **Verteilung**: ein Tätigkeitsblock innerhalb einer Schicht (`verteilung_id`), mit Tätigkeit, optional Maschine und Projekt, Notizen und Artikel-Ausgängen.
- **Pause**: eigener Datensatz in der Schicht (`pause_id`).
- Zeit ohne Verteilung erscheint in der Tagesansicht als „Nicht zugeordnet“.
- Tagesansicht: `GET /arbeitszeit/detail/{mitarbeiterId}/{YYYY-MM-DD}`; je Schicht `table.verteilung_table#verteilung_table_{schichtId}[data-schicht]`, Zeilen `tr.betrieb[id={verteilungId}]` (Spalten: Zeit „HH:MM - HH:MM“, Arbeit, Kunde, Notiz, Kontrolle, Dauer). Auftrags-Zeiten stehen als `tr.auftrag` daneben.
### Endpunkte (getestet mit Mitarbeiter 3309 am 14.10.2026)
Alle `POST` mit `Content-Type: application/json`, sofern nicht anders angegeben. Zeitformat in Formularen: `dd.mm.yy (HH:MM)` (z. B. `14.10.26 (08:00)`), Ortszeit Europe/Berlin, minutengenau.
| Zweck | Request | Antwort |
|---|---|---|
| Tag nachtragen (legt Schicht + Verteilung an) | `POST module/arbeitszeit/api/nachtragen.php`, **form-urlencoded** (jQuery-Kodierung): `form[…]`, `tage[]=YYYY-MM-DD`, `anzahl_datum=1`, `planungAusMitarbeiterId=0` | `{"action":"link","target":"arbeitszeit/plan"}` – **keine ID** |
| Formular einer bestehenden Verteilung laden (lesend) | `module/arbeitszeit/api/editVerteilungForm.php` `{employeeID, date, distributionID}` | HTML `form#verteilung_form[data-id]` + `form#notiz-form` |
| **Neue Verteilung in bestehender Schicht** | `editVerteilungForm.php` `{employeeID, date, shiftID}` | HTML-Formular; **legt sofort eine leere Verteilung an**, ihre ID steht in `#verteilung_form[data-id]`. Das UI löscht sie bei „Abbrechen“ wieder. |
| Verteilung speichern/ändern | `module/arbeitszeit/api/updateVerteilung.php` `{mitarbeiterId, verteilungId, verteilungForm, notizForm}` | `200` leerer Body; Validierungsfehler `422` mit JSON, z. B. `{"verteilungForm":"ganzertag must be present"}` |
| Verteilung löschen | `module/arbeitszeit/api/deleteVerteilung.php` `{distributionID, employeeID, date}` | `200` `null`; war es die letzte Verteilung: `{"action":"link","target":"arbeitszeit"}` und die Schicht wird mitgelöscht (`ist_deleted: 1`) |
| Schicht lesen | `GET module/arbeitszeit/api/getSchicht.php?schicht_id=` | JSON (`timestamp_start`, `timestamp_ende` als `YYYY-MM-DD HH:MM:SS`, `ist_deleted`, …) |
| Schicht ändern | `module/arbeitszeit/api/updateSchicht.php` `{shiftID, timestampStart, timestampEnd}` (Format `dd.mm.yy (HH:MM)`) | `200` `true` |
| Schicht löschen | `module/arbeitszeit/api/deleteSchicht.php` `{shiftID, employeeID, date}` | `{"action":"link","target":"arbeitszeit"}`; Schicht samt Inhalt gelöscht |
| **Arbeitstag beginnen** (laufende Schicht) | `module/auftraege/api/saveSchicht.php` `{vorgang_id: "-1", mitarbeiter_id, formdata: {timestamp_start, timestamp_ende: ""}}` | neue `schicht_id` als Text; Schicht läuft (`timestamp_ende` `0000-00-00 00:00:00`, `beendet: 0`) |
| Menü „Arbeitstag beginnen/beenden“ | `module/arbeitszeit/api/bueroAnundAbmelden.php` `{employeeID}` | `true` – wirkt nur für den angemeldeten Benutzer (Mitarbeiter 6); mit anderer `employeeID` passiert nichts |
| Plausibilität prüfen | `GET module/arbeitszeit/api/checkPlausibelInSchicht.php?timestamp_start&timestamp_ende&mitarbeiterId` | `[]` oder `{timestamp_start:{code,typ,msg}, …}` – nicht getestet |
Weitere vorhandene Endpunkte (nicht getestet): `updatePause.php`/`editPauseForm.php`/`deletePause.php`, `addNotiz.php` (`{verteilungId, notizfeld, verfasser_id}` → `{id,…}`), `startTaetigkeit.php`/`startPause.php`/`beendePause.php`/`beende-schicht.php` (Stempeluhr des eingeloggten Benutzers).
### Felder
`verteilungForm` (alle Werte als String):
| Feld | Bedeutung |
|---|---|
| `timestamp_start`, `timestamp_ende` | `dd.mm.yy (HH:MM)` |
| `taetigkeit_id` | Tätigkeits-ID |
| `alte_taetigkeit_id` | bisherige Tätigkeit (`0` bei neuer Verteilung) |
| `maschine_id` | Maschinen-ID, `0` = keine |
| `projekt_id` | Projekt-ID, `0` = keines |
| `ganzertag` | **Pflicht** (`1` oder `0.5`; nur bei Abwesenheiten fachlich relevant) |
`notizForm`: `notizfeld` (Text; nicht leer → wird als **neue** Notiz an die Verteilung gehängt), `verfasser_id` (Mitarbeiter-ID des eingeloggten Benutzers, steht im Formular).
`nachtragen.php` → `form[…]`: `mitarbeitertyp=1`, `mitarbeiter_id`, `taetigkeit_id`, `maschine_id`, `projekt_id`, `arbeitszeit_start`/`arbeitszeit_ende` (`HH:MM`), `pause_start`/`pause_ende` (leer = keine Pause), `notiz`, `uebernehmen=1` („In Arbeitszeit mit gewählter Dauer übernehmen“; `2` = ohne Dauer, `0` = nur Planung), `kategorie_id=0`, `arbeitzeit_sollstunden=''`.
### Beobachtetes Verhalten
- Nachtragen legt eine Schicht exakt über den angegebenen Zeitraum und darin eine Verteilung an; die Notiz wird als Notizeintrag an der Verteilung gespeichert.
- Eine neue Verteilung über `editVerteilungForm`+`updateVerteilung` **verlängert die Schicht nicht** (Zugeordnet 1,50 h bei Anwesend 0,75 h). Die Schicht muss per `updateSchicht.php` angepasst werden.
- `updateVerteilung` und `deleteVerteilung` **verkürzen** die Schicht dagegen teils automatisch auf die verbleibenden Verteilungen.
- Ändern und Löschen sind möglich; die Referenz ist die `verteilung_id` (Zahl).
## 3. Vorschlag: App-Block → Agrarmonitor
| App | Agrarmonitor |
|---|---|
| Mitarbeiter (`users.am_mitarbeiter_id`) | `mitarbeiterId` / `mitarbeiter_id` / `employeeID` |
| Datum (Start des Arbeitstags) | `date` der Schicht (`YYYY-MM-DD`) |
| Start, Ende (`timestamptz`) | `timestamp_start`/`timestamp_ende` in Europe/Berlin, `dd.mm.yy (HH:MM)` – Sekunden gehen verloren |
| Tätigkeitsart (`activity_types.am_id`) | `taetigkeit_id` |
| Maschine (`machines.am_id`) | `maschine_id` (`0` = keine) |
| Kostenstelle | **kein Feld** – siehe offene Fragen |
| Notiz | `notizForm.notizfeld` (bzw. `form[notiz]` beim Nachtragen) |
| `am_ref` | `verteilung_id` |
Ablauf `createZeitbuchung`:
1. Tagesansicht `arbeitszeit/detail/{mitarbeiterId}/{datum}` laden und Schicht(en) ermitteln.
2. Keine Schicht → `nachtragen.php` für genau diesen Block; danach die Tagesansicht erneut laden und die neue Verteilung über Zeit (und Tätigkeit) finden → `ref`.
3. Schicht vorhanden → `editVerteilungForm.php` mit `shiftID` (liefert die neue ID) → `updateVerteilung.php` mit den Daten; schlägt das fehl, den Platzhalter per `deleteVerteilung.php` wieder entfernen. Danach die Schicht per `updateSchicht.php` auf [frühester Start, spätestes Ende] erweitern.
`updateZeitbuchung`: `updateVerteilung.php` mit vollständigem Formular (alte Tätigkeit aus `editVerteilungForm` mit `distributionID`), danach Schicht erweitern. Notiz nur senden, wenn sie sich geändert hat (sonst entsteht ein doppelter Notizeintrag).
`deleteZeitbuchung`: `deleteVerteilung.php`.
## 4. Entscheidungen (14.10.2026)
- **Kostenstelle:** wird als Zeile `Kostenstelle: …` an die Notiz gehängt.
- **Pausen:** werden als Agrarmonitor-Pause übertragen (`editPauseForm.php` mit `shiftID` legt die Pause an, `updatePause.php` mit `neu: 1` speichert sie, `deletePause.php` `{pauseID, employeeID, date}` löscht). Eine Pause braucht eine vorhandene Schicht.
- **Überschneidungen:** Das Buchen in Agrarmonitor selbst wird für die Mitarbeiter abgeschaltet; der Connector prüft keine Überschneidungen.
- **Umsetzung:** `createZeitbuchung`, `updateZeitbuchung`, `deleteZeitbuchung` mit `ref` = `verteilung:<mitarbeiterId>:<datum>:<id>` bzw. `pause:…`.
- **Notiz ändern:** Notizen löscht `module/notiz/api/delete.php` `{noteID}` (Antwort `true`). Beim Ändern wird die neue Notiz gespeichert und danach die alte gelöscht.
### Echttest (14.10.2026, Mitarbeiter 3309, Tag 11.10.2026)
Über den Connector (eigenes, freigeschaltetes Gerät) angelegt: Block 08:00–09:00 mit Notiz und Kostenstelle (per Nachtragen), Pause 09:00–09:15, Block 09:15–10:00 mit Maschine 5033; danach Block 1 auf 08:55 und Pause auf 09:10 geändert, Notiz ersetzt; alles wieder gelöscht, der Tag ist danach leer. Ergebnis wie erwartet.
**Fund:** Agrarmonitor speichert eine unbekannte `taetigkeit_id` (999999) ohne Fehler – es entsteht eine Verteilung ohne Tätigkeit. Der Connector prüft die Tätigkeit deshalb gegen die Auswahl im Buchungsformular (`nachtragen-form.php` bzw. `editVerteilungForm.php`) und wirft sonst `ZeitbuchungError` (`grund: 'taetigkeit nicht buchbar'`). Maschinen-IDs werden nicht geprüft, weil das Formular nur eine Teilmenge der Maschinen anbietet.
- **Arbeitstag:** Beim Tagesbeginn legt `beginneArbeitstag` eine laufende Schicht an (`saveSchicht.php`, für beliebige Mitarbeiter). Damit kann auch eine Pause die erste Buchung sein. `updateSchicht.php` mit leerem `timestampEnd` lässt eine Schicht laufen, mit Ende wird sie beendet (`beendet: 1`); Tätigkeiten und Pausen in einer laufenden Schicht beenden sie nicht (Echttest 14.10.2026, Mitarbeiter 3309).
## 5. Offene Fragen
1. **Über Mitternacht:** Der Connector trägt bis 23:59 nach und korrigiert Verteilung und Schicht danach auf die echten Zeiten – im Mock getestet, im echten System nicht.
2. **Maschinenpflicht:** Tätigkeiten mit „Maschinen zuordnen = Ja“ verlangen im UI eine Maschine (clientseitige Prüfung über `data-extra`). Ob der Server das ebenfalls erzwingt, ist ungetestet – die App sollte es vorher prüfen (Feld `maschineZuordnen` in `getTaetigkeiten`).
3. **Tätigkeitsauswahl:** Das Buchungsformular bietet nur aktive Tätigkeiten an; Abwesenheiten (Bereich 900) und „800 Pause“ sollten in der App vermutlich ausgeblendet werden.
4. **Verfasser der Notiz:** `verfasser_id` ist der Agrarmonitor-Benutzer des Connectors, nicht der Mitarbeiter.
+917 -2
View File
@@ -20,10 +20,14 @@ import type {
Firma, Firma,
Logger, Logger,
Maschine, Maschine,
MaschineMitGruppe,
MaschinenKategorie, MaschinenKategorie,
Mitarbeiter, Mitarbeiter,
Rechnungsdaten, Rechnungsdaten,
Taetigkeit,
ZeitbuchungDaten,
} from './types'; } from './types';
import { ZeitbuchungError } from './ZeitbuchungError';
type RetryableAxiosRequestConfig = AxiosRequestConfig & { type RetryableAxiosRequestConfig = AxiosRequestConfig & {
_agrarmonitorRetry?: boolean; _agrarmonitorRetry?: boolean;
@@ -46,6 +50,42 @@ type CookieChanges = {
removed: string[]; removed: string[];
}; };
/** Ortszeit Europe/Berlin, minutengenau, in den Formaten, die Agrarmonitor braucht. */
type Zeitpunkt = {
/** YYYY-MM-DD HH:MM - lexikografisch vergleichbar */
vergleich: string;
/** dd.mm.yy (HH:MM) - Formularformat */
formular: string;
datum: string;
uhrzeit: string;
};
type Zeitbuchung = {
art: 'taetigkeit' | 'pause';
mitarbeiterId: number;
datum: string;
start: Zeitpunkt;
ende: Zeitpunkt;
taetigkeitId: number;
maschinenId: number;
notiz: string;
};
type ZeitbuchungRef = {
art: 'taetigkeit' | 'pause';
mitarbeiterId: number;
datum: string;
id: number;
};
/** Start und Ende als YYYY-MM-DD HH:MM (Ortszeit), wie getSchicht.php sie liefert. */
type Schicht = {
id: number;
start: string;
/** null = Schicht laeuft noch (Arbeitstag begonnen, nicht beendet) */
ende: string | null;
};
type AgrarmonitorCustomerPage = { type AgrarmonitorCustomerPage = {
data?: unknown; data?: unknown;
meta?: { meta?: {
@@ -57,6 +97,16 @@ type AgrarmonitorCustomerPage = {
export class AgrarmonitorConnector implements AgrarmonitorConnectorResult { export class AgrarmonitorConnector implements AgrarmonitorConnectorResult {
public http!: AxiosInstance; public http!: AxiosInstance;
private static readonly maxLivesearchSeiten = 500;
private static readonly berlinFormat = new Intl.DateTimeFormat('de-DE', {
timeZone: 'Europe/Berlin',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hourCycle: 'h23',
});
private static readonly s3DateienBaseUrl = 'https://s3-eu-central-1.amazonaws.com/dateien.agrarmonitor.de/07'; private static readonly s3DateienBaseUrl = 'https://s3-eu-central-1.amazonaws.com/dateien.agrarmonitor.de/07';
private readonly baseUrl: string; private readonly baseUrl: string;
@@ -458,6 +508,49 @@ export class AgrarmonitorConnector implements AgrarmonitorConnectorResult {
}); });
} }
async getAlleMaschinen(): Promise<MaschineMitGruppe[]> {
const documents = await this.loadLivesearchSeiten(
'/module/maschinen/livesearch.php',
{ suchstring: '', maschinen_aktiv: 1, maschinengruppe: 0 },
'table.maschinen_table tbody tr[data-id]'
);
const maschinen = new Map<number, MaschineMitGruppe>();
for (const document of documents) {
for (const table of Array.from(document.querySelectorAll<HTMLTableElement>('table.maschinen_table'))) {
const gruppenId = this.parseNumber(table.id.replace('maschinen_table_', ''));
const gruppe = this.vorherigeUeberschrift(table)?.textContent?.trim() ?? '';
for (const row of Array.from(table.querySelectorAll<HTMLTableRowElement>('tbody tr[data-id]'))) {
const id = this.parseNumber(row.getAttribute('data-id'));
const vorhanden = maschinen.get(id);
if (vorhanden) {
if (!vorhanden.gruppenIds.includes(gruppenId)) {
vorhanden.gruppenIds.push(gruppenId);
}
continue;
}
const cells = Array.from(row.querySelectorAll<HTMLTableCellElement>('td'));
maschinen.set(id, {
id,
nummer: this.livesearchText(cells[0]),
name: this.livesearchText(cells[1]),
kennzeichen: this.livesearchText(cells[2]),
aktiv: Boolean(cells[4]?.querySelector('.fa-check, .text-green')),
gruppenId,
gruppe,
gruppenIds: [gruppenId],
});
}
}
}
return Array.from(maschinen.values());
}
async getFirmen(): Promise<Firma[]> { async getFirmen(): Promise<Firma[]> {
const response = await this.http.get('/einstellungen'); const response = await this.http.get('/einstellungen');
@@ -575,6 +668,821 @@ export class AgrarmonitorConnector implements AgrarmonitorConnectorResult {
}); });
} }
async getTaetigkeiten(): Promise<Taetigkeit[]> {
const documents = await this.loadLivesearchSeiten(
'/module/arbeitszeit/taetigkeiten-livesearch.php',
{ taetigkeiten_aktiv: 1 },
'table.taetigkeit_table tbody tr[data-id]'
);
const taetigkeiten = new Map<number, Taetigkeit>();
for (const document of documents) {
for (const table of Array.from(document.querySelectorAll<HTMLTableElement>('table.taetigkeit_table'))) {
const bereichElement = this.vorherigeUeberschrift(table)?.querySelector('.bereich');
for (const row of Array.from(table.querySelectorAll<HTMLTableRowElement>('tbody tr[data-id]'))) {
const id = this.parseNumber(row.getAttribute('data-id'));
const cells = Array.from(row.querySelectorAll<HTMLTableCellElement>('td'));
if (!id || taetigkeiten.has(id)) {
continue;
}
taetigkeiten.set(id, {
id,
uuid: row.getAttribute('data-activityuuid') ?? '',
nummer: cells[0]?.textContent?.trim() ?? '',
name: cells[1]?.textContent?.trim() ?? '',
aktiv: Boolean(row.querySelector('td.toggle .fa-check, td.toggle .text-green')),
bereichId: this.parseNumber(bereichElement?.getAttribute('data-id')),
bereich: bereichElement?.textContent?.trim() ?? '',
maschineZuordnen: this.parseJaNein(cells[2]?.textContent ?? ''),
abwesenheit: this.parseJaNein(cells[3]?.textContent ?? ''),
notizErzwingen: this.parseJaNein(cells[5]?.textContent ?? ''),
});
}
}
}
return Array.from(taetigkeiten.values());
}
/**
* Beginnt den Arbeitstag: legt eine laufende Schicht (Anwesenheit ohne Ende)
* an, wie es das Agrarmonitor-Menue "Arbeitstag beginnen" tut - aber fuer
* beliebige Mitarbeiter (saveSchicht.php statt bueroAnundAbmelden.php, das nur
* fuer den angemeldeten Benutzer wirkt). Laeuft am Tag schon eine Schicht,
* wird deren Referenz geliefert, damit Wiederholungen nichts doppelt anlegen.
*/
async beginneArbeitstag(mitarbeiterId: number, start: Date, options: { datum?: string } = {}): Promise<{ ref: string }> {
const beginn = this.arbeitstagZeitpunkt(mitarbeiterId, start, 'start');
const datum = this.arbeitstagDatum(options.datum, beginn);
const laufend = (await this.ladeSchichten(mitarbeiterId, datum)).find(schicht => schicht.ende === null);
if (laufend) {
this.logDebug('Arbeitstag laeuft bereits', { mitarbeiterId, datum, schichtId: laufend.id });
return { ref: this.arbeitstagRef(mitarbeiterId, datum, laufend.id) };
}
const antwort = await this.zeitbuchungPost(
'/module/auftraege/api/saveSchicht.php',
{
vorgang_id: '-1',
mitarbeiter_id: mitarbeiterId,
formdata: { timestamp_start: beginn.formular, timestamp_ende: '' },
},
'Arbeitstag beginnen'
);
const id = this.parseNumber(antwort);
if (!id) {
throw new ZeitbuchungError('Agrarmonitor hat keine Schicht angelegt', this.zeitbuchungGrund(antwort));
}
return { ref: this.arbeitstagRef(mitarbeiterId, datum, id) };
}
async beendeArbeitstag(ref: string, ende: Date): Promise<void> {
const zielRef = this.parseArbeitstagRef(ref);
const schicht = await this.ladeArbeitstag(zielRef, ref);
await this.arbeitstagSetzen(schicht, schicht.start, this.arbeitstagZeitpunkt(zielRef.mitarbeiterId, ende, 'ende').vergleich);
}
/** Korrigiert Beginn und Ende; `ende: null` laesst den Arbeitstag weiterlaufen. */
async updateArbeitstag(ref: string, zeiten: { start: Date; ende: Date | null }): Promise<void> {
const zielRef = this.parseArbeitstagRef(ref);
const schicht = await this.ladeArbeitstag(zielRef, ref);
const start = this.arbeitstagZeitpunkt(zielRef.mitarbeiterId, zeiten.start, 'start').vergleich;
const ende = zeiten.ende === null ? null : this.arbeitstagZeitpunkt(zielRef.mitarbeiterId, zeiten.ende, 'ende').vergleich;
await this.arbeitstagSetzen(schicht, start, ende);
}
/** Loescht die Schicht samt aller Taetigkeiten und Pausen darin. */
async deleteArbeitstag(ref: string): Promise<void> {
const zielRef = this.parseArbeitstagRef(ref);
await this.zeitbuchungPost(
'/module/arbeitszeit/api/deleteSchicht.php',
{ shiftID: zielRef.id, employeeID: zielRef.mitarbeiterId, date: zielRef.datum },
'Arbeitstag loeschen'
);
}
private arbeitstagZeitpunkt(mitarbeiterId: number, date: Date, feld: 'start' | 'ende'): Zeitpunkt {
if (!Number.isInteger(mitarbeiterId) || mitarbeiterId <= 0) {
throw new ZeitbuchungError('Ungueltige Mitarbeiter-ID', 'mitarbeiterId');
}
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
throw new ZeitbuchungError(feld === 'start' ? 'Ungueltiger Start' : 'Ungueltiges Ende', feld);
}
return this.zeitpunkt(date);
}
private arbeitstagDatum(datum: string | undefined, start: Zeitpunkt): string {
if (datum !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(datum)) {
throw new ZeitbuchungError('Ungueltiger Arbeitstag, erwartet YYYY-MM-DD', 'datum');
}
return datum ?? start.datum;
}
private arbeitstagRef(mitarbeiterId: number, datum: string, schichtId: number): string {
return `schicht:${mitarbeiterId}:${datum}:${schichtId}`;
}
private parseArbeitstagRef(ref: string): { mitarbeiterId: number; datum: string; id: number } {
const match = /^schicht:(\d+):(\d{4}-\d{2}-\d{2}):(\d+)$/.exec(ref);
if (!match) {
throw new ZeitbuchungError(`Ungueltige Arbeitstag-Referenz: ${ref}`, 'ref');
}
return { mitarbeiterId: Number(match[1]), datum: match[2], id: Number(match[3]) };
}
private async ladeArbeitstag(zielRef: { id: number }, ref: string): Promise<Schicht> {
const schicht = await this.ladeSchicht(zielRef.id);
if (!schicht) {
throw new ZeitbuchungError(`Arbeitstag ${ref} nicht gefunden`, 'nicht gefunden');
}
return schicht;
}
private async arbeitstagSetzen(schicht: Schicht, start: string, ende: string | null): Promise<void> {
if (ende !== null && ende <= start) {
throw new ZeitbuchungError('Ende muss mindestens eine Minute nach dem Beginn liegen', 'dauer');
}
await this.schichtSetzen({ id: schicht.id, start, ende });
}
/**
* Bucht einen Taetigkeitsblock oder eine Pause. Gibt es fuer Mitarbeiter und
* Arbeitstag noch keine Schicht, legt `nachtragen.php` Schicht und Verteilung
* an (Pausen brauchen eine vorhandene Schicht). Sonst kommt die Buchung in die
* passende Schicht, die bei Bedarf verlaengert wird.
*/
async createZeitbuchung(daten: ZeitbuchungDaten): Promise<{ ref: string }> {
const buchung = this.zeitbuchungAufbereiten(daten);
const schichten = await this.ladeSchichten(buchung.mitarbeiterId, buchung.datum);
if (schichten.length === 0) {
if (buchung.art === 'pause') {
throw new ZeitbuchungError(
`Pause am ${buchung.datum} kann nicht gebucht werden: keine Schicht vorhanden`,
'keine Schicht vorhanden'
);
}
const id = await this.taetigkeitNachtragen(buchung);
return { ref: this.zeitbuchungRef(buchung, id) };
}
const schicht = this.passendeSchicht(schichten, buchung);
const schichtVorher = { ...schicht };
await this.schichtErweitern(schicht, buchung);
try {
const id =
buchung.art === 'pause'
? await this.pauseAnlegen(buchung, schicht.id)
: await this.verteilungAnlegen(buchung, schicht.id);
return { ref: this.zeitbuchungRef(buchung, id) };
} catch (error) {
if (schicht.start !== schichtVorher.start || schicht.ende !== schichtVorher.ende) {
await this.schichtSetzen(schichtVorher).catch(restoreError => {
this.logger?.warn?.('Schicht konnte nicht zurueckgesetzt werden', {
schichtId: schichtVorher.id,
fehler: restoreError instanceof Error ? restoreError.message : String(restoreError),
});
});
}
throw error;
}
}
async updateZeitbuchung(ref: string, daten: ZeitbuchungDaten): Promise<void> {
const zielRef = this.parseZeitbuchungRef(ref);
const buchung = this.zeitbuchungAufbereiten(daten);
if (buchung.art !== zielRef.art) {
throw new ZeitbuchungError('Art der Buchung (Taetigkeit/Pause) kann nicht geaendert werden', 'art geaendert');
}
if (buchung.mitarbeiterId !== zielRef.mitarbeiterId || buchung.datum !== zielRef.datum) {
throw new ZeitbuchungError(
'Mitarbeiter oder Arbeitstag einer Buchung kann nicht geaendert werden',
'mitarbeiter oder datum geaendert'
);
}
const tagesansicht = await this.ladeTagesansicht(zielRef.mitarbeiterId, zielRef.datum);
const zeilenKlasse = zielRef.art === 'pause' ? 'pause_betrieb' : 'betrieb';
const schichtId = this.parseNumber(
tagesansicht
.querySelector(`tr.${zeilenKlasse}[id="${zielRef.id}"]`)
?.closest('table.verteilung_table')
?.getAttribute('data-schicht')
);
if (!schichtId) {
throw new ZeitbuchungError(`Buchung ${ref} nicht gefunden`, 'nicht gefunden');
}
const schicht = await this.ladeSchicht(schichtId);
if (schicht) {
await this.schichtErweitern(schicht, buchung);
}
if (buchung.art === 'pause') {
await this.pauseSpeichern(buchung, zielRef.id, 0);
return;
}
const formular = await this.ladeVerteilungFormular({
employeeID: zielRef.mitarbeiterId,
date: zielRef.datum,
distributionID: String(zielRef.id),
});
this.taetigkeitPruefen(formular, buchung.taetigkeitId);
const notizen = Array.from(formular.querySelectorAll('#notizen_table tbody tr[data-id]'))
.map(row => ({
id: this.parseNumber(row.getAttribute('data-id')),
text: this.normalisiereText(row.querySelectorAll('td')[2]?.textContent ?? ''),
}))
.filter(notiz => notiz.id > 0);
const notizUnveraendert =
(notizen.length === 0 && !buchung.notiz) ||
(notizen.length === 1 && notizen[0].text === this.normalisiereText(buchung.notiz));
await this.verteilungSpeichern(buchung, zielRef.id, {
alteTaetigkeitId: this.selectedValue(formular, '#taetigkeit_id') || '0',
projektId: this.selectedValue(formular, '#projekt_id') || '0',
verfasserId: formular.querySelector<HTMLInputElement>('#verfasser_id')?.value ?? '',
notiz: notizUnveraendert ? '' : buchung.notiz,
});
// Alte Notizen erst nach erfolgreichem Speichern entfernen.
if (!notizUnveraendert) {
for (const notiz of notizen) {
await this.zeitbuchungPost('/module/notiz/api/delete.php', { noteID: notiz.id }, 'Notiz loeschen');
}
}
}
async deleteZeitbuchung(ref: string): Promise<void> {
const zielRef = this.parseZeitbuchungRef(ref);
if (zielRef.art === 'pause') {
await this.zeitbuchungPost(
'/module/arbeitszeit/api/deletePause.php',
{ pauseID: zielRef.id, employeeID: zielRef.mitarbeiterId, date: zielRef.datum },
'Pause loeschen'
);
return;
}
await this.zeitbuchungPost(
'/module/arbeitszeit/api/deleteVerteilung.php',
{ distributionID: zielRef.id, employeeID: zielRef.mitarbeiterId, date: zielRef.datum },
'Buchung loeschen'
);
}
private zeitbuchungAufbereiten(daten: ZeitbuchungDaten): Zeitbuchung {
if (!Number.isInteger(daten.mitarbeiterId) || daten.mitarbeiterId <= 0) {
throw new ZeitbuchungError('Ungueltige Mitarbeiter-ID', 'mitarbeiterId');
}
if (!(daten.start instanceof Date) || Number.isNaN(daten.start.getTime())) {
throw new ZeitbuchungError('Ungueltiger Start', 'start');
}
if (!(daten.ende instanceof Date) || Number.isNaN(daten.ende.getTime())) {
throw new ZeitbuchungError('Ungueltiges Ende', 'ende');
}
if (daten.datum !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(daten.datum)) {
throw new ZeitbuchungError('Ungueltiger Arbeitstag, erwartet YYYY-MM-DD', 'datum');
}
const start = this.zeitpunkt(daten.start);
const ende = this.zeitpunkt(daten.ende);
if (ende.vergleich <= start.vergleich) {
throw new ZeitbuchungError('Ende muss mindestens eine Minute nach dem Start liegen', 'dauer');
}
const basis = {
mitarbeiterId: daten.mitarbeiterId,
datum: daten.datum ?? start.datum,
start,
ende,
};
if (daten.art === 'pause') {
return { ...basis, art: 'pause', taetigkeitId: 0, maschinenId: 0, notiz: '' };
}
if (!Number.isInteger(daten.taetigkeitId) || daten.taetigkeitId <= 0) {
throw new ZeitbuchungError('Ungueltige Taetigkeits-ID', 'taetigkeitId');
}
const maschinenId = daten.maschinenId ?? 0;
if (!Number.isInteger(maschinenId) || maschinenId < 0) {
throw new ZeitbuchungError('Ungueltige Maschinen-ID', 'maschinenId');
}
const kostenstelle = daten.kostenstelle?.trim();
const notiz = [daten.notiz?.trim(), kostenstelle ? `Kostenstelle: ${kostenstelle}` : '']
.filter(Boolean)
.join('\n');
return { ...basis, art: 'taetigkeit', taetigkeitId: daten.taetigkeitId, maschinenId, notiz };
}
/** Rundet auf die volle Minute - Agrarmonitor speichert keine Sekunden. */
private zeitpunkt(date: Date): Zeitpunkt {
const gerundet = new Date(Math.round(date.getTime() / 60000) * 60000);
const teile = Object.fromEntries(
AgrarmonitorConnector.berlinFormat.formatToParts(gerundet).map(teil => [teil.type, teil.value])
);
const datum = `${teile.year}-${teile.month}-${teile.day}`;
const uhrzeit = `${teile.hour}:${teile.minute}`;
return {
vergleich: `${datum} ${uhrzeit}`,
formular: this.vergleichZuFormular(`${datum} ${uhrzeit}`),
datum,
uhrzeit,
};
}
/** YYYY-MM-DD HH:MM -> dd.mm.yy (HH:MM) */
private vergleichZuFormular(wert: string): string {
const [datum, uhrzeit] = wert.split(' ');
const [jahr, monat, tag] = datum.split('-');
return `${tag}.${monat}.${jahr.slice(2)} (${uhrzeit})`;
}
private zeitbuchungRef(buchung: Zeitbuchung, id: number): string {
return `${buchung.art === 'pause' ? 'pause' : 'verteilung'}:${buchung.mitarbeiterId}:${buchung.datum}:${id}`;
}
private parseZeitbuchungRef(ref: string): ZeitbuchungRef {
const match = /^(verteilung|pause):(\d+):(\d{4}-\d{2}-\d{2}):(\d+)$/.exec(ref);
if (!match) {
throw new ZeitbuchungError(`Ungueltige Zeitbuchungs-Referenz: ${ref}`, 'ref');
}
return {
art: match[1] === 'pause' ? 'pause' : 'taetigkeit',
mitarbeiterId: Number(match[2]),
datum: match[3],
id: Number(match[4]),
};
}
private async ladeTagesansicht(mitarbeiterId: number, datum: string): Promise<Document> {
const response = await this.http.get(`/arbeitszeit/detail/${mitarbeiterId}/${datum}`);
await this.saveSession();
return this.parseHtmlDocument(response.data);
}
private async ladeSchichten(mitarbeiterId: number, datum: string): Promise<Schicht[]> {
const tagesansicht = await this.ladeTagesansicht(mitarbeiterId, datum);
const ids = new Set(
Array.from(tagesansicht.querySelectorAll('table.verteilung_table[data-schicht]'))
.map(table => this.parseNumber(table.getAttribute('data-schicht')))
.filter(id => id > 0)
);
const schichten: Schicht[] = [];
for (const id of ids) {
const schicht = await this.ladeSchicht(id);
if (schicht) {
schichten.push(schicht);
}
}
return schichten;
}
private async ladeSchicht(schichtId: number): Promise<Schicht | null> {
const response = await this.http.get('/module/arbeitszeit/api/getSchicht.php', {
params: { schicht_id: schichtId },
});
await this.saveSession();
const data = (typeof response.data === 'string' ? JSON.parse(response.data || 'null') : response.data) as {
timestamp_start?: string;
timestamp_ende?: string;
ist_deleted?: number | string;
} | null;
if (!data?.timestamp_start || Number(data.ist_deleted) === 1) {
return null;
}
const start = data.timestamp_start.slice(0, 16);
const ende = data.timestamp_ende && !data.timestamp_ende.startsWith('0000') ? data.timestamp_ende.slice(0, 16) : null;
return { id: schichtId, start, ende };
}
/** Schicht, die sich mit der Buchung ueberschneidet, sonst die letzte davor, sonst die erste. */
private passendeSchicht(schichten: Schicht[], buchung: Zeitbuchung): Schicht {
return (
schichten.find(
schicht => schicht.start <= buchung.ende.vergleich && (schicht.ende === null || buchung.start.vergleich <= schicht.ende)
) ??
schichten.filter(schicht => schicht.start <= buchung.start.vergleich).at(-1) ??
schichten[0]
);
}
/**
* Neue Verteilungen verlaengern die Schicht in Agrarmonitor nicht von selbst.
* Eine laufende Schicht bleibt offen - nur ihr Start wird bei Bedarf vorgezogen.
*/
private async schichtErweitern(schicht: Schicht, buchung: Zeitbuchung): Promise<void> {
const start = buchung.start.vergleich < schicht.start ? buchung.start.vergleich : schicht.start;
const ende =
schicht.ende === null || buchung.ende.vergleich <= schicht.ende ? schicht.ende : buchung.ende.vergleich;
if (start === schicht.start && ende === schicht.ende) {
return;
}
await this.schichtSetzen({ id: schicht.id, start, ende });
schicht.start = start;
schicht.ende = ende;
}
private async schichtSetzen(schicht: Schicht): Promise<void> {
await this.zeitbuchungPost(
'/module/arbeitszeit/api/updateSchicht.php',
{
shiftID: schicht.id,
timestampStart: this.vergleichZuFormular(schicht.start),
// Leeres Ende laesst die Schicht laufen.
timestampEnd: schicht.ende === null ? '' : this.vergleichZuFormular(schicht.ende),
},
'Schicht anpassen'
);
}
/**
* Erste Buchung des Tages: nachtragen.php legt Schicht und Verteilung an,
* liefert aber keine ID. Die Verteilung wird danach in der Tagesansicht gesucht.
*/
private async taetigkeitNachtragen(buchung: Zeitbuchung): Promise<number> {
const nachtragenFormular = await this.http.get('/module/arbeitszeit/api/nachtragen-form.php');
await this.saveSession();
this.taetigkeitPruefen(this.parseHtmlDocument(nachtragenFormular.data), buchung.taetigkeitId);
const amArbeitstag = buchung.start.datum === buchung.datum && buchung.ende.datum === buchung.datum;
const form: Record<string, string> = {
taetigkeit_id: String(buchung.taetigkeitId),
projekt_id: '0',
arbeitszeit_start: buchung.start.uhrzeit,
// Nachtragen kennt nur Uhrzeiten; ueber Mitternacht wird unten korrigiert.
arbeitszeit_ende: amArbeitstag ? buchung.ende.uhrzeit : '23:59',
arbeitzeit_sollstunden: '',
pause_start: '',
pause_ende: '',
maschine_id: String(buchung.maschinenId),
notiz: buchung.notiz,
uebernehmen: '1',
mitarbeitertyp: '1',
mitarbeiter_id: String(buchung.mitarbeiterId),
kategorie_id: '0',
};
const body = new URLSearchParams();
for (const [name, value] of Object.entries(form)) {
body.append(`form[${name}]`, value);
}
body.append('tage[]', buchung.datum);
body.append('anzahl_datum', '1');
body.append('planungAusMitarbeiterId', '0');
const response = await this.http.post('/module/arbeitszeit/api/nachtragen.php', body, {
...this.formPostConfig('/arbeitszeit'),
validateStatus: status => status >= 200 && status < 500,
});
await this.saveSession();
this.zeitbuchungAntwortPruefen(response, 'Nachtragen');
const tagesansicht = await this.ladeTagesansicht(buchung.mitarbeiterId, buchung.datum);
const zeilen = Array.from(tagesansicht.querySelectorAll('table.verteilung_table tr.betrieb[id]'));
if (zeilen.length !== 1) {
throw new ZeitbuchungError(
`Nachgetragene Buchung am ${buchung.datum} nicht eindeutig gefunden (${zeilen.length} Buchungen)`,
'nicht eindeutig'
);
}
const id = this.parseNumber(zeilen[0].id);
if (!amArbeitstag) {
const formular = await this.ladeVerteilungFormular({
employeeID: buchung.mitarbeiterId,
date: buchung.datum,
distributionID: String(id),
});
await this.verteilungSpeichern(buchung, id, {
alteTaetigkeitId: String(buchung.taetigkeitId),
projektId: '0',
verfasserId: formular.querySelector<HTMLInputElement>('#verfasser_id')?.value ?? '',
notiz: '',
});
const schicht = await this.ladeSchicht(
this.parseNumber(zeilen[0].closest('table.verteilung_table')?.getAttribute('data-schicht'))
);
if (schicht) {
await this.schichtErweitern(schicht, buchung);
}
}
return id;
}
/**
* editVerteilungForm.php mit shiftID legt sofort eine leere Verteilung an
* (das Agrarmonitor-UI loescht sie bei "Abbrechen"). Schlaegt das Speichern
* fehl, wird dieser Platzhalter wieder entfernt.
*/
private async verteilungAnlegen(buchung: Zeitbuchung, schichtId: number): Promise<number> {
const formular = await this.ladeVerteilungFormular({
employeeID: buchung.mitarbeiterId,
date: buchung.datum,
shiftID: schichtId,
});
const id = this.parseNumber(formular.querySelector('#verteilung_form')?.getAttribute('data-id'));
if (!id) {
throw new ZeitbuchungError('Agrarmonitor hat keine neue Buchung angelegt', 'keine verteilung_id');
}
try {
this.taetigkeitPruefen(formular, buchung.taetigkeitId);
await this.verteilungSpeichern(buchung, id, {
alteTaetigkeitId: '0',
projektId: '0',
verfasserId: formular.querySelector<HTMLInputElement>('#verfasser_id')?.value ?? '',
notiz: buchung.notiz,
});
} catch (error) {
await this.zeitbuchungPost(
'/module/arbeitszeit/api/deleteVerteilung.php',
{ distributionID: id, employeeID: buchung.mitarbeiterId, date: buchung.datum },
'Platzhalter loeschen'
).catch(() => undefined);
throw error;
}
return id;
}
private async ladeVerteilungFormular(body: Record<string, string | number>): Promise<Document> {
const html = await this.zeitbuchungPost(
'/module/arbeitszeit/api/editVerteilungForm.php',
body,
'Buchungsformular laden'
);
const formular = this.parseHtmlDocument(html);
if (!formular.querySelector('#verteilung_form')) {
throw new ZeitbuchungError('Buchungsformular nicht gefunden', 'kein verteilung_form');
}
return formular;
}
private async verteilungSpeichern(
buchung: Zeitbuchung,
id: number,
formular: { alteTaetigkeitId: string; projektId: string; verfasserId: string; notiz: string }
): Promise<void> {
await this.zeitbuchungPost(
'/module/arbeitszeit/api/updateVerteilung.php',
{
mitarbeiterId: buchung.mitarbeiterId,
verteilungId: String(id),
verteilungForm: {
timestamp_start: buchung.start.formular,
timestamp_ende: buchung.ende.formular,
alte_taetigkeit_id: formular.alteTaetigkeitId,
taetigkeit_id: String(buchung.taetigkeitId),
ganzertag: '1',
maschine_id: String(buchung.maschinenId),
projekt_id: formular.projektId,
},
// Nicht leere notizfeld werden als neue Notiz an die Verteilung gehaengt.
notizForm: {
notizfeld: formular.notiz,
verfasser_id: formular.verfasserId,
},
},
'Buchung speichern'
);
}
/** Wie bei Verteilungen legt editPauseForm.php mit shiftID sofort eine Pause an. */
private async pauseAnlegen(buchung: Zeitbuchung, schichtId: number): Promise<number> {
const html = await this.zeitbuchungPost(
'/module/arbeitszeit/api/editPauseForm.php',
{ employeeID: buchung.mitarbeiterId, date: buchung.datum, shiftID: schichtId },
'Pausenformular laden'
);
const id = this.parseNumber(this.parseHtmlDocument(html).querySelector('#pause_form')?.getAttribute('data-id'));
if (!id) {
throw new ZeitbuchungError('Agrarmonitor hat keine neue Pause angelegt', 'keine pause_id');
}
try {
await this.pauseSpeichern(buchung, id, 1);
} catch (error) {
await this.zeitbuchungPost(
'/module/arbeitszeit/api/deletePause.php',
{ pauseID: id, employeeID: buchung.mitarbeiterId, date: buchung.datum },
'Platzhalter loeschen'
).catch(() => undefined);
throw error;
}
return id;
}
private async pauseSpeichern(buchung: Zeitbuchung, id: number, neu: 0 | 1): Promise<void> {
await this.zeitbuchungPost(
'/module/arbeitszeit/api/updatePause.php',
{
mitarbeiterId: buchung.mitarbeiterId,
pause_id: String(id),
pauseForm: {
pause_start: buchung.start.formular,
pause_ende: buchung.ende.formular,
},
neu,
},
'Pause speichern'
);
}
/**
* JSON-POST an die Arbeitszeit-API. 4xx (z. B. 422 mit Grund im Body) wird
* als ZeitbuchungError geworfen; Netzwerk- und 5xx-Fehler bleiben Axios-Fehler.
*/
private async zeitbuchungPost(url: string, body: unknown, aktion: string): Promise<unknown> {
const response = await this.http.post(url, body, {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json, */*',
'X-Requested-With': 'XMLHttpRequest',
Referer: `${this.baseUrl}/arbeitszeit`,
Origin: this.baseUrl,
},
validateStatus: status => status >= 200 && status < 500,
});
await this.saveSession();
this.zeitbuchungAntwortPruefen(response, aktion);
return response.data;
}
private zeitbuchungAntwortPruefen(response: AxiosResponse, aktion: string): void {
if (response.status >= 200 && response.status < 300) {
return;
}
const grund = this.zeitbuchungGrund(response.data);
throw new ZeitbuchungError(`${aktion} fehlgeschlagen (HTTP ${response.status}): ${grund}`, grund, response.status);
}
/** 422-Antworten sehen so aus: {"verteilungForm":"ganzertag must be present"} */
private zeitbuchungGrund(data: unknown): string {
if (data && typeof data === 'object') {
const texte = Object.values(data as Record<string, unknown>)
.flatMap(wert => (wert && typeof wert === 'object' ? Object.values(wert as Record<string, unknown>) : [wert]))
.filter((wert): wert is string => typeof wert === 'string' && wert.trim() !== '');
return texte.length ? texte.join('; ') : JSON.stringify(data).slice(0, 500);
}
return String(data ?? '').trim().slice(0, 500) || 'keine Antwort';
}
/**
* Agrarmonitor speichert auch unbekannte oder inaktive Taetigkeits-IDs (dann
* ohne Taetigkeit). Deshalb gegen die Auswahl im Buchungsformular pruefen.
*/
private taetigkeitPruefen(formular: Document, taetigkeitId: number): void {
const buchbar = Array.from(formular.querySelectorAll<HTMLOptionElement>('select#taetigkeit_id option')).map(
option => option.value
);
if (!buchbar.includes(String(taetigkeitId))) {
throw new ZeitbuchungError(
`Taetigkeit ${taetigkeitId} ist in Agrarmonitor nicht buchbar (unbekannt oder inaktiv)`,
'taetigkeit nicht buchbar'
);
}
}
private selectedValue(document: Document, selector: string): string {
const select = document.querySelector<HTMLSelectElement>(selector);
return select?.querySelector<HTMLOptionElement>('option[selected]')?.value ?? select?.value ?? '';
}
private normalisiereText(text: string): string {
return text.replace(/\s+/g, ' ').trim();
}
/**
* Laedt eine Livesearch-Liste Seite fuer Seite. Die Seitenzahl steht in
* `.pagination[data-pages]`; zusaetzlich bricht die Schleife ab, sobald eine
* Seite keine neuen Zeilen bringt (Agrarmonitor ignoriert `seite` teils).
*/
private async loadLivesearchSeiten(
url: string,
params: Record<string, string | number>,
rowSelector: string
): Promise<Document[]> {
const documents: Document[] = [];
const bekannteZeilen = new Set<string>();
for (let seite = 1; seite <= AgrarmonitorConnector.maxLivesearchSeiten; seite += 1) {
const response = await this.http.get(url, { params: { ...params, seite } });
await this.saveSession();
const document = this.parseHtmlDocument(response.data);
const neueZeilen = Array.from(document.querySelectorAll(rowSelector))
.map(row => `${row.closest('table')?.id ?? ''}:${row.getAttribute('data-id') ?? row.id}`)
.filter(key => !bekannteZeilen.has(key));
if (neueZeilen.length === 0) {
break;
}
neueZeilen.forEach(key => bekannteZeilen.add(key));
documents.push(document);
const seiten = this.parseNumber(document.querySelector('.pagination')?.getAttribute('data-pages'));
if (seite >= seiten) {
break;
}
}
return documents;
}
private vorherigeUeberschrift(element: Element): Element | null {
let sibling = element.previousElementSibling;
while (sibling && !/^H[1-6]$/.test(sibling.tagName)) {
sibling = sibling.previousElementSibling;
}
return sibling;
}
/** Agrarmonitor zeigt leere Werte in Livesearch-Tabellen als "-" an. */
private livesearchText(cell: HTMLTableCellElement | undefined): string {
const text = cell?.textContent?.trim() ?? '';
return text === '-' ? '' : text;
}
private createHttpClient(): AxiosInstance { private createHttpClient(): AxiosInstance {
const client = wrapper( const client = wrapper(
axios.create({ axios.create({
@@ -949,8 +1857,15 @@ export class AgrarmonitorConnector implements AgrarmonitorConnectorResult {
const document = this.parseHtmlDocument(response.data); const document = this.parseHtmlDocument(response.data);
const receivedStatus = document.querySelector<HTMLElement>('#receivedStatus'); const receivedStatus = document.querySelector<HTMLElement>('#receivedStatus');
const receivedText = receivedStatus?.textContent?.trim() ?? ''; const receivedText = receivedStatus?.textContent?.trim() ?? '';
const parentParts = (receivedStatus?.parentElement?.textContent ?? '').split('-'); // Buchungsstatus steht in #bookedStatus. Fallback für das alte Layout: der Teil der
const bookingText = parentParts.at(-1)?.trim() ?? ''; // Statuszeile mit "gebucht" (nicht einfach der letzte, dahinter folgt "Zahlung erfassen").
const bookingText =
document.querySelector<HTMLElement>('#bookedStatus')?.textContent?.trim() ??
(receivedStatus?.parentElement?.textContent ?? '')
.split('-')
.map(part => part.trim())
.find(part => /gebucht/i.test(part)) ??
'';
return { return {
eingangsDatum: eingangsDatum:
+16
View File
@@ -0,0 +1,16 @@
/**
* Agrarmonitor hat eine Zeitbuchung fachlich abgelehnt (z. B. HTTP 422 mit
* Grund im Body) oder die Buchung ist so nicht moeglich. Ein erneuter Versuch
* mit denselben Daten hilft nicht - anders als bei Netzwerk- oder 5xx-Fehlern,
* die als normale Fehler durchgereicht werden.
*/
export class ZeitbuchungError extends Error {
constructor(
message: string,
public readonly grund: string,
public readonly status?: number
) {
super(message);
this.name = 'ZeitbuchungError';
}
}
+6
View File
@@ -3,10 +3,16 @@ export { FileCookieStore } from './cookie-store/FileCookieStore';
export { MemoryCookieStore } from './cookie-store/MemoryCookieStore'; export { MemoryCookieStore } from './cookie-store/MemoryCookieStore';
export { AgrarmonitorConnector } from './AgrarmonitorConnector'; export { AgrarmonitorConnector } from './AgrarmonitorConnector';
export { createAgrarmonitorClient } from './createAgrarmonitorClient'; export { createAgrarmonitorClient } from './createAgrarmonitorClient';
export { ZeitbuchungError } from './ZeitbuchungError';
export type { export type {
AgrarmonitorConnectorOptions, AgrarmonitorConnectorOptions,
AgrarmonitorConnectorResult, AgrarmonitorConnectorResult,
CookieEncryptor, CookieEncryptor,
CookieStore, CookieStore,
Logger, Logger,
MaschineMitGruppe,
PausenbuchungDaten,
Taetigkeit,
TaetigkeitsbuchungDaten,
ZeitbuchungDaten,
} from './types'; } from './types';
+61
View File
@@ -56,11 +56,20 @@ export interface AgrarmonitorConnectorResult {
getCustomerById(id: number): Promise<AgrarmonitorApiCustomer>; getCustomerById(id: number): Promise<AgrarmonitorApiCustomer>;
getMaschinenKategorien(): Promise<MaschinenKategorie[]>; getMaschinenKategorien(): Promise<MaschinenKategorie[]>;
getMaschinen(gruppenId: number, suchstring?: string): Promise<Maschine[]>; getMaschinen(gruppenId: number, suchstring?: string): Promise<Maschine[]>;
getAlleMaschinen(): Promise<MaschineMitGruppe[]>;
getFirmen(): Promise<Firma[]>; getFirmen(): Promise<Firma[]>;
getArtikelEinheiten(): Promise<ArtikelEinheit[]>; getArtikelEinheiten(): Promise<ArtikelEinheit[]>;
getArtikelKategorien(): Promise<ArtikelKategorie[]>; getArtikelKategorien(): Promise<ArtikelKategorie[]>;
getArtikel(artikelGruppe: number, suchstring?: string): Promise<Artikel[]>; getArtikel(artikelGruppe: number, suchstring?: string): Promise<Artikel[]>;
getMitarbeiter(suchstring?: string, mitarbeitergruppe?: number): Promise<Mitarbeiter[]>; getMitarbeiter(suchstring?: string, mitarbeitergruppe?: number): Promise<Mitarbeiter[]>;
getTaetigkeiten(): Promise<Taetigkeit[]>;
beginneArbeitstag(mitarbeiterId: number, start: Date, options?: { datum?: string }): Promise<{ ref: string }>;
beendeArbeitstag(ref: string, ende: Date): Promise<void>;
updateArbeitstag(ref: string, zeiten: { start: Date; ende: Date | null }): Promise<void>;
deleteArbeitstag(ref: string): Promise<void>;
createZeitbuchung(daten: ZeitbuchungDaten): Promise<{ ref: string }>;
updateZeitbuchung(ref: string, daten: ZeitbuchungDaten): Promise<void>;
deleteZeitbuchung(ref: string): Promise<void>;
} }
export interface AgrarmonitorFreischaltungStatus { export interface AgrarmonitorFreischaltungStatus {
@@ -170,6 +179,14 @@ export interface Maschine {
aktiv: boolean; aktiv: boolean;
} }
export interface MaschineMitGruppe extends Maschine {
/** Erste Maschinenkategorie, in der die Maschine gelistet ist */
gruppenId: number;
gruppe: string;
/** Alle Maschinenkategorien - eine Maschine kann in mehreren stehen */
gruppenIds: number[];
}
export interface Firma { export interface Firma {
id: number; id: number;
bezeichnung: string; bezeichnung: string;
@@ -199,3 +216,47 @@ export interface Mitarbeiter {
vorname: string; vorname: string;
aktiv: boolean; aktiv: boolean;
} }
export interface Taetigkeit {
id: number;
uuid: string;
nummer: string;
name: string;
aktiv: boolean;
bereichId: number;
/** Ueberschrift des Bereichs, z. B. "100 Werkstatt (Mobil sichtbar)" */
bereich: string;
/** Spalte "Maschinen zuordnen" */
maschineZuordnen: boolean;
abwesenheit: boolean;
notizErzwingen: boolean;
}
interface ZeitbuchungBasis {
/** Agrarmonitor-Mitarbeiter-ID */
mitarbeiterId: number;
start: Date;
ende: Date;
/**
* Arbeitstag (YYYY-MM-DD), zu dessen Schicht die Buchung gehoert.
* Standard: Datum von `start` in Europe/Berlin. Fuer Bloecke nach
* Mitternacht den Tag des Arbeitsbeginns angeben.
*/
datum?: string;
}
export interface TaetigkeitsbuchungDaten extends ZeitbuchungBasis {
art?: 'taetigkeit';
taetigkeitId: number;
maschinenId?: number;
/** Agrarmonitor hat kein Kostenstellen-Feld - landet als Zeile in der Notiz */
kostenstelle?: string;
notiz?: string;
}
/** Pausen haben in Agrarmonitor keine Notiz. */
export interface PausenbuchungDaten extends ZeitbuchungBasis {
art: 'pause';
}
export type ZeitbuchungDaten = TaetigkeitsbuchungDaten | PausenbuchungDaten;
+93
View File
@@ -0,0 +1,93 @@
const assert = require('node:assert/strict');
const http = require('node:http');
const test = require('node:test');
const { AgrarmonitorConnector, MemoryCookieStore } = require('../dist');
// Kopfzeile der Detailseite /eingangsrechnungen/detail/<id>, wie sie AM aktuell liefert.
const statusZeile = inhalt => `<html><body><div class="content"><h3>${inhalt}</h3></div></body></html>`;
const seiten = {
// Seit AM den Link "Zahlung erfassen" hinter dem Buchungsstatus anzeigt.
387605: statusZeile(`
<a id="receivedStatus" class="pointer">Empfangen am 21.09.26</a> -
<a id="bookedStatus" class="pointer">Gebucht am 27.09.26</a>
- <a class="pointer" id="addPayment">Zahlung erfassen</a>`),
387606: statusZeile(`
<a id="receivedStatus" class="pointer">Empfangen am 21.09.26</a> -
<a id="bookedStatus" class="pointer">Nicht gebucht</a>`),
387607: statusZeile(`
<a id="receivedStatus" class="pointer">Nicht empfangen</a> -
<a id="bookedStatus" class="pointer">Nicht gebucht</a>`),
// Altes Layout ohne id am Buchungsstatus.
387608: statusZeile(`
<a id="receivedStatus" class="pointer">Empfangen am 01.10.26</a> - Gebucht am 02.10.26`),
};
async function startConnector(t) {
const server = http.createServer((req, res) => {
req.resume();
req.on('end', () => {
if (req.method === 'POST' && req.url === '/login/api/login.php') {
res.writeHead(200, { 'Content-Type': 'application/json', 'Set-Cookie': 'agrar_session=test; Path=/' });
res.end(JSON.stringify({ success: true }));
return;
}
if (req.method === 'GET' && req.url === '/') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('<html><body>Mein AM</body></html>');
return;
}
const match = /^\/eingangsrechnungen\/detail\/(\d+)$/.exec(req.url ?? '');
if (req.method === 'GET' && match && seiten[match[1]]) {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(seiten[match[1]]);
return;
}
res.writeHead(404);
res.end('not found');
});
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
t.after(() => new Promise(resolve => server.close(resolve)));
const connector = new AgrarmonitorConnector({
baseUrl: `http://127.0.0.1:${server.address().port}`,
username: 'demo',
password: 'secret',
cookieStore: new MemoryCookieStore(),
});
await connector.init();
return connector;
}
const tag = d => d && [d.getFullYear(), d.getMonth() + 1, d.getDate()];
test('Detailseite: Buchungsdatum trotz nachfolgendem "Zahlung erfassen"', async t => {
const connector = await startConnector(t);
const meta = await connector.getEingangsrechnungDetailMeta(387605);
assert.deepEqual(tag(meta.eingangsDatum), [2026, 9, 21]);
assert.deepEqual(tag(meta.buchungsDatum), [2026, 9, 27]);
});
test('Detailseite: nicht gebucht liefert kein Buchungsdatum', async t => {
const connector = await startConnector(t);
const meta = await connector.getEingangsrechnungDetailMeta(387606);
assert.deepEqual(tag(meta.eingangsDatum), [2026, 9, 21]);
assert.equal(meta.buchungsDatum, null);
});
test('Detailseite: nicht empfangen und nicht gebucht', async t => {
const connector = await startConnector(t);
const meta = await connector.getEingangsrechnungDetailMeta(387607);
assert.equal(meta.eingangsDatum, null);
assert.equal(meta.buchungsDatum, null);
});
test('Detailseite: altes Layout ohne #bookedStatus', async t => {
const connector = await startConnector(t);
const meta = await connector.getEingangsrechnungDetailMeta(387608);
assert.deepEqual(tag(meta.eingangsDatum), [2026, 10, 1]);
assert.deepEqual(tag(meta.buchungsDatum), [2026, 10, 2]);
});
+303
View File
@@ -0,0 +1,303 @@
const assert = require('node:assert/strict');
const http = require('node:http');
const test = require('node:test');
const { AgrarmonitorConnector, MemoryCookieStore } = require('../dist');
const loginPage = `
<html>
<head><title>Anmeldung - AGRARMONITOR</title></head>
<body>
<form>
<input name="nonce" value="test-nonce" />
<button>Einloggen</button>
</form>
</body>
</html>
`;
const taetigkeitHead = `
<thead><tr>
<td class="breite-einheit">#</td><td width="24%">Bezeichnung</td><td width="13%">Maschinen zuordnen</td>
<td width="12%">Abwesenheit</td><td width="12%">Als Urlaub werten</td><td width="13%">Notiz erzwingen</td>
<td width="13%">Wartungsplan</td><td width="13%">Erweiterte Effizienz</td>
<td class="breite-icon" data-sort-method="none" align="center">Aktiv</td>
<td class="breite-icon" data-sort-method="none" align="center">&nbsp;</td>
</tr></thead>
`;
// Gekuerzte Antwort von /module/arbeitszeit/taetigkeiten-livesearch.php?taetigkeiten_aktiv=1
const taetigkeitenLivesearch = `
<h2><b class="bereich" data-id="17">100 Werkstatt (Mobil sichtbar) </b></h2>
<table width="100%" class="alternate sortable taetigkeit_table table-fixed">
${taetigkeitHead}
<tbody>
<tr data-id="869" data-activityUUID="0191763e-2f7d-7c53-838d-f60b3c7bd0a6">
<td>100</td><td>Reparatur / Wartung / Waschen etc. von Maschinen</td>
<td>Ja</td><td>Nein</td><td>Nein</td><td>Ja</td><td>Ja</td><td>Nein</td>
<td align="center" class="block toggle"><i class="fa fa-check text-green fa-lg"></i></td>
<td align="center" class="block"></td>
</tr>
<tr data-id="15" data-activityUUID="0191763e-2cae-7d34-98d6-ec160935ae42">
<td>110</td><td>Wartung</td>
<td>Ja</td><td>Nein</td><td>Nein</td><td>Nein</td><td>Ja</td><td>Nein</td>
<td align="center" class="block toggle"><i class="fa fa-close text-red fa-lg"></i></td>
<td align="center" class="block"></td>
</tr>
</tbody>
</table>
<br />
<h2><b class="bereich" data-id="19">900 Abwesenheit (Mobil nicht sichtbar) </b></h2>
<table width="100%" class="alternate sortable taetigkeit_table table-fixed">
${taetigkeitHead}
<tbody>
<tr data-id="819" data-activityUUID="0191763e-2f3c-7766-be96-4bf6933e5677">
<td>950</td><td>Feiertag</td>
<td>Nein</td><td>Ja</td><td>Nein</td><td>Nein</td><td>Nein</td><td>Nein</td>
<td align="center" class="block toggle"><i class="fa fa-check text-green fa-lg"></i></td>
<td align="center" class="block"></td>
</tr>
</tbody>
</table>
<script>admin.Arbeitszeit.Arbeitszeit.taetigkeitenLivesearch("de-DE", {"taetigkeiten":1});</script>
`;
function maschinenTabelle(gruppenId, gruppe, rows) {
return `
<h2>${gruppe}</h2>
<table width="100%" class="sortable clickable alternate maschinen_table table-fixed" id="maschinen_table_${gruppenId}">
<thead><tr>
<td width="10%">#</td><td width="50%">Bezeichnung</td><td width="15%">Kennzeichen</td><td width="25%">Zähler</td>
<td align="center" class="breite-50" data-sort-method="none">Aktiv</td>
<td align="center" class="block breite-icon" data-sort-method="none"><input type="checkbox" class="checkall"></td>
</tr></thead>
<tbody>${rows}</tbody>
</table>
`;
}
function maschinenZeile(id, nummer, name, kennzeichen, aktiv) {
return `
<tr id="${id}" data-id="${id}">
<td>${nummer}</td><td> ${name} </td><td>${kennzeichen}</td><td>-</td>
<td align="center" class="block toggle"><i class="fa ${aktiv ? 'fa-check text-green' : 'fa-close text-red'} fa-lg"></i></td>
<td align="center" class="block"><input type="checkbox" name="maschinen[]" value="${id}"></td>
</tr>
`;
}
const pagination = seiten => `<div class="pagination" data-firma="" data-pages="${seiten}"></div>`;
// Seite 1: eine Maschine steht in zwei Kategorien, Seite 2: weitere Maschine.
const maschinenSeite1 =
maschinenTabelle(
7,
'Schlepper',
maschinenZeile(5033, '12', 'Testschlepper 724', 'XX-AB 724', true) +
maschinenZeile(3696, '-', 'Einrichtung', '-', false)
) +
maschinenTabelle(893, 'Zugmaschinen GmbH', maschinenZeile(5033, '12', 'Testschlepper 724', 'XX-AB 724', true));
const maschinenSeite2 = maschinenTabelle(13, 'LKWs', maschinenZeile(7087, '40', 'Test-LKW', 'XX-CD 40', true));
function createServer({ maschinenSeiten }) {
const requests = [];
let loggedIn = false;
const server = http.createServer((req, res) => {
const url = new URL(req.url, 'http://localhost');
requests.push({ method: req.method, path: url.pathname, params: Object.fromEntries(url.searchParams) });
if (req.method === 'POST' && url.pathname === '/login/api/login.php') {
req.resume();
req.on('end', () => {
loggedIn = true;
res.writeHead(200, { 'Content-Type': 'application/json', 'Set-Cookie': 'agrar_session=test; Path=/' });
res.end(JSON.stringify({ success: true }));
});
return;
}
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
if (!loggedIn) {
res.end(loginPage);
return;
}
if (url.pathname === '/') {
res.end('<html><body>Mein AM</body></html>');
return;
}
if (url.pathname === '/module/arbeitszeit/taetigkeiten-livesearch.php') {
// Agrarmonitor ignoriert "seite" hier und liefert immer alles.
res.end(taetigkeitenLivesearch);
return;
}
if (url.pathname === '/module/maschinen/livesearch.php') {
const seite = Number(url.searchParams.get('seite'));
res.end(maschinenSeiten(seite));
return;
}
res.writeHead(404);
res.end('not found');
});
return {
server,
requests,
expireSession() {
loggedIn = false;
},
};
}
async function startConnector(t, serverOptions = { maschinenSeiten: () => maschinenSeite1 + pagination(0) }) {
const { server, requests, expireSession } = createServer(serverOptions);
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
t.after(() => new Promise(resolve => server.close(resolve)));
const connector = new AgrarmonitorConnector({
baseUrl: `http://127.0.0.1:${server.address().port}`,
username: 'demo',
password: 'secret',
cookieStore: new MemoryCookieStore(),
});
await connector.init();
return { connector, requests, expireSession };
}
const livesearchRequests = (requests, path) => requests.filter(request => request.path === path);
test('getTaetigkeiten liest aktive und inaktive Taetigkeiten mit Bereich', async t => {
const { connector, requests } = await startConnector(t);
const taetigkeiten = await connector.getTaetigkeiten();
assert.deepEqual(taetigkeiten, [
{
id: 869,
uuid: '0191763e-2f7d-7c53-838d-f60b3c7bd0a6',
nummer: '100',
name: 'Reparatur / Wartung / Waschen etc. von Maschinen',
aktiv: true,
bereichId: 17,
bereich: '100 Werkstatt (Mobil sichtbar)',
maschineZuordnen: true,
abwesenheit: false,
notizErzwingen: true,
},
{
id: 15,
uuid: '0191763e-2cae-7d34-98d6-ec160935ae42',
nummer: '110',
name: 'Wartung',
aktiv: false,
bereichId: 17,
bereich: '100 Werkstatt (Mobil sichtbar)',
maschineZuordnen: true,
abwesenheit: false,
notizErzwingen: false,
},
{
id: 819,
uuid: '0191763e-2f3c-7766-be96-4bf6933e5677',
nummer: '950',
name: 'Feiertag',
aktiv: true,
bereichId: 19,
bereich: '900 Abwesenheit (Mobil nicht sichtbar)',
maschineZuordnen: false,
abwesenheit: true,
notizErzwingen: false,
},
]);
const calls = livesearchRequests(requests, '/module/arbeitszeit/taetigkeiten-livesearch.php');
assert.equal(calls.length, 1, 'ohne Pagination nur eine Seite laden');
assert.deepEqual(calls[0].params, { taetigkeiten_aktiv: '1', seite: '1' });
});
test('getAlleMaschinen laedt alle Seiten und fasst Maschinen aus mehreren Kategorien zusammen', async t => {
const { connector, requests } = await startConnector(t, {
maschinenSeiten: seite => (seite === 1 ? maschinenSeite1 : maschinenSeite2) + pagination(2),
});
const maschinen = await connector.getAlleMaschinen();
assert.deepEqual(maschinen, [
{
id: 5033,
nummer: '12',
name: 'Testschlepper 724',
kennzeichen: 'XX-AB 724',
aktiv: true,
gruppenId: 7,
gruppe: 'Schlepper',
gruppenIds: [7, 893],
},
{
id: 3696,
nummer: '',
name: 'Einrichtung',
kennzeichen: '',
aktiv: false,
gruppenId: 7,
gruppe: 'Schlepper',
gruppenIds: [7],
},
{
id: 7087,
nummer: '40',
name: 'Test-LKW',
kennzeichen: 'XX-CD 40',
aktiv: true,
gruppenId: 13,
gruppe: 'LKWs',
gruppenIds: [13],
},
]);
const calls = livesearchRequests(requests, '/module/maschinen/livesearch.php');
assert.deepEqual(
calls.map(call => call.params),
[
{ suchstring: '', maschinen_aktiv: '1', maschinengruppe: '0', seite: '1' },
{ suchstring: '', maschinen_aktiv: '1', maschinengruppe: '0', seite: '2' },
]
);
});
test('getAlleMaschinen bricht ab, wenn eine Seite keine neuen Zeilen bringt', async t => {
// data-pages behauptet 5 Seiten, ab Seite 2 kommt immer dasselbe.
const { connector, requests } = await startConnector(t, {
maschinenSeiten: seite => (seite === 1 ? maschinenSeite1 : maschinenSeite2) + pagination(5),
});
const maschinen = await connector.getAlleMaschinen();
assert.deepEqual(
maschinen.map(maschine => maschine.id),
[5033, 3696, 7087]
);
assert.equal(livesearchRequests(requests, '/module/maschinen/livesearch.php').length, 3);
});
test('getTaetigkeiten meldet sich bei abgelaufener Session neu an', async t => {
const { connector, requests, expireSession } = await startConnector(t);
const logins = () => requests.filter(request => request.path === '/login/api/login.php').length;
assert.equal(logins(), 1, 'init soll genau einmal einloggen');
expireSession();
const taetigkeiten = await connector.getTaetigkeiten();
assert.equal(taetigkeiten.length, 3);
assert.equal(logins(), 2, 'abgelaufene Session soll genau einen erneuten Login ausloesen');
assert.equal(livesearchRequests(requests, '/module/arbeitszeit/taetigkeiten-livesearch.php').length, 2);
});
+715
View File
@@ -0,0 +1,715 @@
const assert = require('node:assert/strict');
const http = require('node:http');
const test = require('node:test');
const { AgrarmonitorConnector, MemoryCookieStore, ZeitbuchungError } = require('../dist');
const loginPage = `
<html>
<head><title>Anmeldung - AGRARMONITOR</title></head>
<body><form><input name="nonce" value="test-nonce" /><button>Einloggen</button></form></body>
</html>
`;
// "dd.mm.yy (HH:MM)" -> "YYYY-MM-DD HH:MM"
function formularZuVergleich(wert) {
const [, tag, monat, jahr, uhrzeit] = /^(\d{2})\.(\d{2})\.(\d{2}) \((\d{2}:\d{2})\)$/.exec(wert);
return `20${jahr}-${monat}-${tag} ${uhrzeit}`;
}
/**
* Nachbildung der Arbeitszeit-Endpunkte, so wie sie sich am 14.10.2026 im
* echten Agrarmonitor verhalten haben (siehe docs/zeitbuchung-analyse.md).
*/
function createServer() {
const requests = [];
const state = { schichten: new Map(), verteilungen: new Map(), pausen: new Map() };
let loggedIn = false;
let naechsteId = 100;
const neueId = () => (naechsteId += 1);
const json = (res, status, data) => {
res.writeHead(status, { 'Content-Type': 'application/json' });
res.end(data === undefined ? '' : JSON.stringify(data));
};
const html = (res, body) => {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(body);
};
const schichtAufraeumen = schichtId => {
const leer = ![...state.verteilungen.values()].some(v => v.schichtId === schichtId);
if (leer) {
state.schichten.get(schichtId).deleted = true;
}
return leer;
};
const tagesansicht = (mitarbeiterId, datum) =>
[...state.schichten.entries()]
.filter(([, s]) => !s.deleted && s.mitarbeiterId === mitarbeiterId && s.datum === datum)
.map(([schichtId]) => {
const zeilen = [
...[...state.verteilungen.entries()]
.filter(([, v]) => v.schichtId === schichtId)
.map(([id, v]) => `<tr id="${id}" class=" betrieb "><td>${v.start.slice(11)} - ${v.ende.slice(11)}</td></tr>`),
...[...state.pausen.entries()]
.filter(([, p]) => p.schichtId === schichtId)
.map(([id, p]) => `<tr id="${id}" class="pause_betrieb text-orange"><td>${p.start.slice(11)} - ${p.ende.slice(11)}</td></tr>`),
];
return `<table class="alternate verteilung_table table-fixed" id="verteilung_table_${schichtId}" data-schicht="${schichtId}"><tbody>${zeilen.join('')}</tbody></table>`;
})
.join('');
// Buchbar sind nur aktive Taetigkeiten; 999 ist unbekannt.
const taetigkeitAuswahl = ausgewaehlt => `
<select name="taetigkeit_id" id="taetigkeit_id">
<option value="870"${ausgewaehlt === '870' ? ' selected="selected"' : ''}>110 Arbeiten</option>
<option value="869"${ausgewaehlt === '869' ? ' selected="selected"' : ''}>100 Reparatur</option>
</select>`;
const verteilungFormular = id => {
const v = state.verteilungen.get(id);
return `
<form id="verteilung_form" data-id="${id}">
<input type="text" name="timestamp_start" id="timestamp_start" value="">
<input type="hidden" name="alte_taetigkeit_id" id="alte_taetigkeit_id" value="${v.taetigkeitId}">
${taetigkeitAuswahl(v.taetigkeitId)}
<select name="projekt_id" id="projekt_id">
<option value="0">Kein Projekt</option>
<option value="12"${v.projektId === '12' ? ' selected="selected"' : ''}>Projekt</option>
</select>
</form>
<form id="notiz-form">
<textarea name="notizfeld" id="notizfeld"></textarea>
<input type="text" name="verfasser_id" id="verfasser_id" value="6">
</form>
<table id="notizen_table"><tbody>
${v.notizen.map(n => `<tr data-id="${n.id}"><td>14.10.26 (11:15)</td><td>Connector</td><td class="notiz">${n.text.replace(/\n/g, '<br />\n')}</td></tr>`).join('')}
</tbody></table>
`;
};
const server = http.createServer((req, res) => {
let body = '';
req.on('data', chunk => {
body += chunk;
});
req.on('end', () => {
const url = new URL(req.url, 'http://localhost');
const istJson = (req.headers['content-type'] ?? '').includes('application/json');
const daten = istJson && body ? JSON.parse(body) : body;
requests.push({ method: req.method, path: url.pathname, body: daten });
if (req.method === 'POST' && url.pathname === '/login/api/login.php') {
loggedIn = true;
res.writeHead(200, { 'Content-Type': 'application/json', 'Set-Cookie': 'agrar_session=test; Path=/' });
res.end(JSON.stringify({ success: true }));
return;
}
if (!loggedIn) {
html(res, loginPage);
return;
}
const detail = /^\/arbeitszeit\/detail\/(\d+)\/(\d{4}-\d{2}-\d{2})$/.exec(url.pathname);
if (detail) {
html(res, `<html><body>${tagesansicht(Number(detail[1]), detail[2])}</body></html>`);
return;
}
switch (url.pathname) {
case '/':
html(res, '<html><body>Mein AM</body></html>');
return;
case '/module/arbeitszeit/api/nachtragen-form.php':
html(res, `<form id="nachtragen_form">${taetigkeitAuswahl('0')}</form>`);
return;
case '/module/arbeitszeit/api/getSchicht.php': {
const id = Number(url.searchParams.get('schicht_id'));
const s = state.schichten.get(id);
json(res, 200, {
id,
mitarbeiter_id: s.mitarbeiterId,
timestamp_start: `${s.start}:00`,
// Laufende Schicht: Agrarmonitor liefert ein Null-Datum.
timestamp_ende: s.ende === null ? '0000-00-00 00:00:00' : `${s.ende}:00`,
ist_deleted: s.deleted ? 1 : 0,
});
return;
}
case '/module/arbeitszeit/api/nachtragen.php': {
const params = new URLSearchParams(body);
const datum = params.get('tage[]');
const schichtId = neueId();
const start = `${datum} ${params.get('form[arbeitszeit_start]')}`;
const ende = `${datum} ${params.get('form[arbeitszeit_ende]')}`;
state.schichten.set(schichtId, { mitarbeiterId: Number(params.get('form[mitarbeiter_id]')), datum, start, ende });
const notiz = params.get('form[notiz]');
state.verteilungen.set(neueId(), {
schichtId,
start,
ende,
taetigkeitId: params.get('form[taetigkeit_id]'),
maschinenId: params.get('form[maschine_id]'),
projektId: '0',
notizen: notiz ? [{ id: neueId(), text: notiz }] : [],
});
json(res, 200, { action: 'link', target: 'arbeitszeit/plan' });
return;
}
case '/module/arbeitszeit/api/editVerteilungForm.php': {
let id = Number(daten.distributionID);
if (daten.shiftID) {
// Wie im echten System: Formular fuer "neu" legt sofort einen Platzhalter an.
const s = state.schichten.get(daten.shiftID);
id = neueId();
state.verteilungen.set(id, {
schichtId: daten.shiftID,
start: s.start,
ende: s.start,
taetigkeitId: '0',
maschinenId: '0',
projektId: '0',
notizen: [],
});
}
html(res, state.verteilungen.has(id) ? verteilungFormular(id) : '');
return;
}
case '/module/arbeitszeit/api/updateVerteilung.php': {
const form = daten.verteilungForm;
if (form.ganzertag === undefined) {
json(res, 422, { verteilungForm: 'ganzertag must be present' });
return;
}
if (form.maschine_id === '999') {
json(res, 422, { verteilungForm: 'Maschine ungueltig' });
return;
}
const v = state.verteilungen.get(Number(daten.verteilungId));
Object.assign(v, {
start: formularZuVergleich(form.timestamp_start),
ende: formularZuVergleich(form.timestamp_ende),
taetigkeitId: form.taetigkeit_id,
maschinenId: form.maschine_id,
projektId: form.projekt_id,
});
if (daten.notizForm.notizfeld) {
v.notizen.push({ id: neueId(), text: daten.notizForm.notizfeld });
}
json(res, 200);
return;
}
case '/module/arbeitszeit/api/deleteVerteilung.php': {
const v = state.verteilungen.get(daten.distributionID);
state.verteilungen.delete(daten.distributionID);
json(res, 200, schichtAufraeumen(v.schichtId) ? { action: 'link', target: 'arbeitszeit' } : null);
return;
}
case '/module/arbeitszeit/api/editPauseForm.php': {
const s = state.schichten.get(daten.shiftID);
const id = neueId();
state.pausen.set(id, { schichtId: daten.shiftID, start: s.start, ende: s.ende });
html(res, `<form id="pause_form" data-id="${id}"><input name="pause_start" id="timestamp_start"></form>`);
return;
}
case '/module/arbeitszeit/api/updatePause.php': {
const p = state.pausen.get(Number(daten.pause_id));
p.start = formularZuVergleich(daten.pauseForm.pause_start);
p.ende = formularZuVergleich(daten.pauseForm.pause_ende);
json(res, 200, 0);
return;
}
case '/module/arbeitszeit/api/deletePause.php':
state.pausen.delete(daten.pauseID);
json(res, 200, null);
return;
case '/module/arbeitszeit/api/updateSchicht.php': {
const s = state.schichten.get(daten.shiftID);
s.start = formularZuVergleich(daten.timestampStart);
s.ende = daten.timestampEnd === '' ? null : formularZuVergleich(daten.timestampEnd);
json(res, 200, true);
return;
}
case '/module/auftraege/api/saveSchicht.php': {
const id = neueId();
const start = formularZuVergleich(daten.formdata.timestamp_start);
state.schichten.set(id, {
mitarbeiterId: daten.mitarbeiter_id,
datum: start.slice(0, 10),
start,
ende: daten.formdata.timestamp_ende === '' ? null : formularZuVergleich(daten.formdata.timestamp_ende),
});
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(String(id));
return;
}
case '/module/arbeitszeit/api/deleteSchicht.php':
state.schichten.get(daten.shiftID).deleted = true;
json(res, 200, { action: 'link', target: 'arbeitszeit' });
return;
case '/module/notiz/api/delete.php':
for (const v of state.verteilungen.values()) {
v.notizen = v.notizen.filter(n => n.id !== daten.noteID);
}
json(res, 200, true);
return;
default:
res.writeHead(404);
res.end('not found');
}
});
});
return {
server,
requests,
state,
neueId,
expireSession() {
loggedIn = false;
},
};
}
async function startConnector(t) {
const fake = createServer();
await new Promise(resolve => fake.server.listen(0, '127.0.0.1', resolve));
t.after(() => new Promise(resolve => fake.server.close(resolve)));
const connector = new AgrarmonitorConnector({
baseUrl: `http://127.0.0.1:${fake.server.address().port}`,
username: 'demo',
password: 'secret',
cookieStore: new MemoryCookieStore(),
});
await connector.init();
return { connector, ...fake };
}
/** Legt eine Schicht mit einer Verteilung an, wie nach einem ersten Nachtragen. */
function schichtAnlegen(state, neueId, { start = '2026-10-14 08:00', ende = '2026-10-14 09:00' } = {}) {
const schichtId = neueId();
const verteilungId = neueId();
state.schichten.set(schichtId, { mitarbeiterId: 3309, datum: '2026-10-14', start, ende });
state.verteilungen.set(verteilungId, {
schichtId,
start,
ende,
taetigkeitId: '870',
maschinenId: '0',
projektId: '12',
notizen: [{ id: neueId(), text: 'Alte Notiz' }],
});
return { schichtId, verteilungId };
}
const posts = (requests, path) => requests.filter(r => r.method === 'POST' && r.path === path);
const schreibendePosts = requests =>
requests.filter(r => r.method === 'POST' && r.path.startsWith('/module/') && !r.path.endsWith('Form.php'));
// 14.10.2026 ist Sommerzeit: 06:00Z = 08:00 Uhr in Berlin.
const block = (start, ende, extra = {}) => ({
mitarbeiterId: 3309,
start: new Date(start),
ende: new Date(ende),
taetigkeitId: 870,
...extra,
});
test('createZeitbuchung traegt ohne Schicht Schicht und Taetigkeit nach', async t => {
const { connector, requests, state } = await startConnector(t);
const { ref } = await connector.createZeitbuchung(
block('2026-10-14T06:00:00Z', '2026-10-14T07:00:20Z', {
maschinenId: 5033,
notiz: 'Hydraulik abgedichtet',
kostenstelle: '4711',
})
);
const [nachtragen] = posts(requests, '/module/arbeitszeit/api/nachtragen.php');
const params = new URLSearchParams(nachtragen.body);
assert.equal(params.get('form[mitarbeiter_id]'), '3309');
assert.equal(params.get('form[mitarbeitertyp]'), '1');
assert.equal(params.get('form[taetigkeit_id]'), '870');
assert.equal(params.get('form[maschine_id]'), '5033');
assert.equal(params.get('form[arbeitszeit_start]'), '08:00');
assert.equal(params.get('form[arbeitszeit_ende]'), '09:00', 'Sekunden werden auf die Minute gerundet');
assert.equal(params.get('form[pause_start]'), '');
assert.equal(params.get('form[uebernehmen]'), '1');
assert.equal(params.get('form[notiz]'), 'Hydraulik abgedichtet\nKostenstelle: 4711');
assert.deepEqual(params.getAll('tage[]'), ['2026-10-14']);
const [[verteilungId, verteilung]] = [...state.verteilungen.entries()];
assert.equal(ref, `verteilung:3309:2026-10-14:${verteilungId}`);
assert.equal(verteilung.start, '2026-10-14 08:00');
assert.equal(verteilung.ende, '2026-10-14 09:00');
});
test('createZeitbuchung bucht in die vorhandene Schicht und verlaengert sie', async t => {
const { connector, requests, state, neueId } = await startConnector(t);
const { schichtId } = schichtAnlegen(state, neueId);
const { ref } = await connector.createZeitbuchung(
block('2026-10-14T07:00:00Z', '2026-10-14T08:00:00Z', { taetigkeitId: 869, maschinenId: 5033, notiz: 'Zweiter Block' })
);
assert.deepEqual(posts(requests, '/module/arbeitszeit/api/editVerteilungForm.php')[0].body, {
employeeID: 3309,
date: '2026-10-14',
shiftID: schichtId,
});
const verteilungId = Number(ref.split(':').at(-1));
assert.equal(ref, `verteilung:3309:2026-10-14:${verteilungId}`);
assert.deepEqual(posts(requests, '/module/arbeitszeit/api/updateVerteilung.php')[0].body, {
mitarbeiterId: 3309,
verteilungId: String(verteilungId),
verteilungForm: {
timestamp_start: '14.10.26 (09:00)',
timestamp_ende: '14.10.26 (10:00)',
alte_taetigkeit_id: '0',
taetigkeit_id: '869',
ganzertag: '1',
maschine_id: '5033',
projekt_id: '0',
},
notizForm: { notizfeld: 'Zweiter Block', verfasser_id: '6' },
});
assert.deepEqual(posts(requests, '/module/arbeitszeit/api/updateSchicht.php').map(r => r.body), [
{ shiftID: schichtId, timestampStart: '14.10.26 (08:00)', timestampEnd: '14.10.26 (10:00)' },
]);
assert.equal(posts(requests, '/module/arbeitszeit/api/nachtragen.php').length, 0);
});
test('createZeitbuchung bucht eine Pause in die Schicht', async t => {
const { connector, requests, state, neueId } = await startConnector(t);
const { schichtId } = schichtAnlegen(state, neueId);
const { ref } = await connector.createZeitbuchung({
art: 'pause',
mitarbeiterId: 3309,
start: new Date('2026-10-14T07:00:00Z'),
ende: new Date('2026-10-14T07:30:00Z'),
});
const [[pauseId, pause]] = [...state.pausen.entries()];
assert.equal(ref, `pause:3309:2026-10-14:${pauseId}`);
assert.deepEqual(pause, { schichtId, start: '2026-10-14 09:00', ende: '2026-10-14 09:30' });
assert.deepEqual(posts(requests, '/module/arbeitszeit/api/updatePause.php')[0].body, {
mitarbeiterId: 3309,
pause_id: String(pauseId),
pauseForm: { pause_start: '14.10.26 (09:00)', pause_ende: '14.10.26 (09:30)' },
neu: 1,
});
});
test('createZeitbuchung lehnt eine Pause ohne Schicht ab', async t => {
const { connector, requests } = await startConnector(t);
await assert.rejects(
() =>
connector.createZeitbuchung({
art: 'pause',
mitarbeiterId: 3309,
start: new Date('2026-10-14T07:00:00Z'),
ende: new Date('2026-10-14T07:30:00Z'),
}),
error => error instanceof ZeitbuchungError && /keine Schicht vorhanden/.test(error.message)
);
assert.equal(schreibendePosts(requests).length, 0);
});
test('createZeitbuchung meldet den Grund einer 422 und raeumt Platzhalter und Schicht auf', async t => {
const { connector, requests, state, neueId } = await startConnector(t);
const { schichtId, verteilungId } = schichtAnlegen(state, neueId);
await assert.rejects(
() => connector.createZeitbuchung(block('2026-10-14T07:00:00Z', '2026-10-14T08:00:00Z', { maschinenId: 999 })),
error =>
error instanceof ZeitbuchungError &&
error.status === 422 &&
error.grund === 'Maschine ungueltig' &&
/Buchung speichern fehlgeschlagen \(HTTP 422\): Maschine ungueltig/.test(error.message)
);
assert.deepEqual([...state.verteilungen.keys()], [verteilungId], 'Platzhalter wurde wieder geloescht');
assert.equal(posts(requests, '/module/arbeitszeit/api/deleteVerteilung.php').length, 1);
const schicht = state.schichten.get(schichtId);
assert.deepEqual([schicht.start, schicht.ende], ['2026-10-14 08:00', '2026-10-14 09:00'], 'Schicht zurueckgesetzt');
});
test('createZeitbuchung ueber Mitternacht korrigiert Verteilung und Schicht nach dem Nachtragen', async t => {
const { connector, requests, state } = await startConnector(t);
await connector.createZeitbuchung(
block('2026-10-14T21:30:00Z', '2026-10-14T22:30:00Z', { datum: '2026-10-14' })
);
const params = new URLSearchParams(posts(requests, '/module/arbeitszeit/api/nachtragen.php')[0].body);
assert.equal(params.get('form[arbeitszeit_start]'), '23:30');
assert.equal(params.get('form[arbeitszeit_ende]'), '23:59');
const update = posts(requests, '/module/arbeitszeit/api/updateVerteilung.php')[0].body;
assert.equal(update.verteilungForm.timestamp_start, '14.10.26 (23:30)');
assert.equal(update.verteilungForm.timestamp_ende, '15.10.26 (00:30)');
assert.equal(update.notizForm.notizfeld, '', 'Notiz nicht doppelt anlegen');
const [schicht] = state.schichten.values();
assert.deepEqual([schicht.start, schicht.ende], ['2026-10-14 23:30', '2026-10-15 00:30']);
});
// Im echten System gespeichert statt abgelehnt (Echttest 14.10.2026) - der Connector muss pruefen.
test('createZeitbuchung lehnt eine nicht buchbare Taetigkeit ab', async t => {
const { connector, requests, state, neueId } = await startConnector(t);
await assert.rejects(
() => connector.createZeitbuchung(block('2026-10-14T06:00:00Z', '2026-10-14T07:00:00Z', { taetigkeitId: 999 })),
error => error instanceof ZeitbuchungError && error.grund === 'taetigkeit nicht buchbar'
);
assert.equal(posts(requests, '/module/arbeitszeit/api/nachtragen.php').length, 0, 'ohne Schicht nichts nachtragen');
const { verteilungId } = schichtAnlegen(state, neueId);
await assert.rejects(
() => connector.createZeitbuchung(block('2026-10-14T07:00:00Z', '2026-10-14T08:00:00Z', { taetigkeitId: 999 })),
error => error instanceof ZeitbuchungError && error.grund === 'taetigkeit nicht buchbar'
);
assert.equal(posts(requests, '/module/arbeitszeit/api/updateVerteilung.php').length, 0);
assert.deepEqual([...state.verteilungen.keys()], [verteilungId], 'Platzhalter wurde wieder geloescht');
await assert.rejects(
() =>
connector.updateZeitbuchung(
`verteilung:3309:2026-10-14:${verteilungId}`,
block('2026-10-14T06:00:00Z', '2026-10-14T07:00:00Z', { taetigkeitId: 999 })
),
error => error instanceof ZeitbuchungError && error.grund === 'taetigkeit nicht buchbar'
);
assert.equal(posts(requests, '/module/arbeitszeit/api/updateVerteilung.php').length, 0);
});
test('updateZeitbuchung aendert die Verteilung und ersetzt die Notiz', async t => {
const { connector, requests, state, neueId } = await startConnector(t);
const { verteilungId } = schichtAnlegen(state, neueId);
const alteNotizId = state.verteilungen.get(verteilungId).notizen[0].id;
await connector.updateZeitbuchung(
`verteilung:3309:2026-10-14:${verteilungId}`,
block('2026-10-14T06:00:00Z', '2026-10-14T06:45:00Z', { taetigkeitId: 869, maschinenId: 5033, notiz: 'Neue Notiz' })
);
assert.deepEqual(posts(requests, '/module/arbeitszeit/api/updateVerteilung.php')[0].body, {
mitarbeiterId: 3309,
verteilungId: String(verteilungId),
verteilungForm: {
timestamp_start: '14.10.26 (08:00)',
timestamp_ende: '14.10.26 (08:45)',
alte_taetigkeit_id: '870',
taetigkeit_id: '869',
ganzertag: '1',
maschine_id: '5033',
projekt_id: '12',
},
notizForm: { notizfeld: 'Neue Notiz', verfasser_id: '6' },
});
assert.deepEqual(posts(requests, '/module/notiz/api/delete.php').map(r => r.body), [{ noteID: alteNotizId }]);
assert.deepEqual(state.verteilungen.get(verteilungId).notizen.map(n => n.text), ['Neue Notiz']);
});
test('updateZeitbuchung laesst eine unveraenderte Notiz stehen', async t => {
const { connector, requests, state, neueId } = await startConnector(t);
const { verteilungId } = schichtAnlegen(state, neueId);
await connector.updateZeitbuchung(
`verteilung:3309:2026-10-14:${verteilungId}`,
block('2026-10-14T06:00:00Z', '2026-10-14T06:45:00Z', { notiz: 'Alte Notiz' })
);
assert.equal(posts(requests, '/module/arbeitszeit/api/updateVerteilung.php')[0].body.notizForm.notizfeld, '');
assert.equal(posts(requests, '/module/notiz/api/delete.php').length, 0);
});
test('updateZeitbuchung aendert eine Pause', async t => {
const { connector, requests, state, neueId } = await startConnector(t);
const { schichtId } = schichtAnlegen(state, neueId);
const pauseId = neueId();
state.pausen.set(pauseId, { schichtId, start: '2026-10-14 08:30', ende: '2026-10-14 08:45' });
await connector.updateZeitbuchung(`pause:3309:2026-10-14:${pauseId}`, {
art: 'pause',
mitarbeiterId: 3309,
start: new Date('2026-10-14T06:35:00Z'),
ende: new Date('2026-10-14T06:50:00Z'),
});
assert.deepEqual(posts(requests, '/module/arbeitszeit/api/updatePause.php')[0].body, {
mitarbeiterId: 3309,
pause_id: String(pauseId),
pauseForm: { pause_start: '14.10.26 (08:35)', pause_ende: '14.10.26 (08:50)' },
neu: 0,
});
});
test('updateZeitbuchung lehnt ungueltige Referenzen und Wechsel von Art oder Tag ab', async t => {
const { connector, requests, state, neueId } = await startConnector(t);
const { verteilungId } = schichtAnlegen(state, neueId);
const daten = block('2026-10-14T06:00:00Z', '2026-10-14T06:45:00Z');
await assert.rejects(() => connector.updateZeitbuchung('1180803', daten), ZeitbuchungError);
await assert.rejects(
() => connector.updateZeitbuchung(`pause:3309:2026-10-14:${verteilungId}`, daten),
/Art der Buchung/
);
await assert.rejects(
() => connector.updateZeitbuchung(`verteilung:3309:2026-10-13:${verteilungId}`, daten),
/Arbeitstag/
);
await assert.rejects(
() => connector.updateZeitbuchung(`verteilung:3309:2026-10-14:999999`, daten),
/nicht gefunden/
);
assert.equal(schreibendePosts(requests).length, 0);
});
test('deleteZeitbuchung loescht Verteilung und Pause', async t => {
const { connector, requests, state, neueId } = await startConnector(t);
const { schichtId, verteilungId } = schichtAnlegen(state, neueId);
const pauseId = neueId();
state.pausen.set(pauseId, { schichtId, start: '2026-10-14 08:30', ende: '2026-10-14 08:45' });
await connector.deleteZeitbuchung(`pause:3309:2026-10-14:${pauseId}`);
await connector.deleteZeitbuchung(`verteilung:3309:2026-10-14:${verteilungId}`);
assert.deepEqual(posts(requests, '/module/arbeitszeit/api/deletePause.php')[0].body, {
pauseID: pauseId,
employeeID: 3309,
date: '2026-10-14',
});
assert.deepEqual(posts(requests, '/module/arbeitszeit/api/deleteVerteilung.php')[0].body, {
distributionID: verteilungId,
employeeID: 3309,
date: '2026-10-14',
});
assert.equal(state.verteilungen.size, 0);
assert.equal(state.pausen.size, 0);
});
test('createZeitbuchung prueft die Eingaben vor dem ersten Request', async t => {
const { connector, requests } = await startConnector(t);
const anzahlVorher = requests.length;
await assert.rejects(
() => connector.createZeitbuchung(block('2026-10-14T06:00:00Z', '2026-10-14T06:00:20Z')),
/mindestens eine Minute/
);
await assert.rejects(
() => connector.createZeitbuchung(block('2026-10-14T06:00:00Z', '2026-10-14T07:00:00Z', { taetigkeitId: 0 })),
/Taetigkeits-ID/
);
assert.equal(requests.length, anzahlVorher);
});
test('createZeitbuchung meldet sich bei abgelaufener Session neu an', async t => {
const { connector, requests, state, expireSession } = await startConnector(t);
const logins = () => posts(requests, '/login/api/login.php').length;
assert.equal(logins(), 1);
expireSession();
const { ref } = await connector.createZeitbuchung(block('2026-10-14T06:00:00Z', '2026-10-14T07:00:00Z'));
assert.equal(logins(), 2, 'genau ein erneuter Login');
assert.match(ref, /^verteilung:3309:2026-10-14:\d+$/);
assert.equal(state.verteilungen.size, 1);
});
test('beginneArbeitstag legt eine laufende Schicht an und liefert die vorhandene bei Wiederholung', async t => {
const { connector, requests, state } = await startConnector(t);
const { ref } = await connector.beginneArbeitstag(3309, new Date('2026-10-14T04:58:40Z'));
assert.deepEqual(posts(requests, '/module/auftraege/api/saveSchicht.php').map(r => r.body), [
{ vorgang_id: '-1', mitarbeiter_id: 3309, formdata: { timestamp_start: '14.10.26 (06:59)', timestamp_ende: '' } },
]);
const [[schichtId, schicht]] = [...state.schichten.entries()];
assert.equal(ref, `schicht:3309:2026-10-14:${schichtId}`);
assert.equal(schicht.ende, null, 'Schicht laeuft');
const wiederholt = await connector.beginneArbeitstag(3309, new Date('2026-10-14T04:58:40Z'));
assert.equal(wiederholt.ref, ref);
assert.equal(posts(requests, '/module/auftraege/api/saveSchicht.php').length, 1);
});
test('Pause und Taetigkeit in laufender Schicht lassen sie offen', async t => {
const { connector, requests, state } = await startConnector(t);
const { ref } = await connector.beginneArbeitstag(3309, new Date('2026-10-14T05:00:00Z'));
const schichtId = Number(ref.split(':').at(-1));
const pause = await connector.createZeitbuchung({
art: 'pause',
mitarbeiterId: 3309,
start: new Date('2026-10-14T05:00:00Z'),
ende: new Date('2026-10-14T05:15:00Z'),
});
assert.match(pause.ref, /^pause:3309:2026-10-14:\d+$/, 'Pause als erste Buchung des Tages');
// Block beginnt vor der Schicht: Start wird vorgezogen, Ende bleibt offen.
await connector.createZeitbuchung(block('2026-10-14T04:50:00Z', '2026-10-14T05:00:00Z'));
assert.equal(posts(requests, '/module/arbeitszeit/api/nachtragen.php').length, 0);
assert.deepEqual(posts(requests, '/module/arbeitszeit/api/updateSchicht.php').map(r => r.body), [
{ shiftID: schichtId, timestampStart: '14.10.26 (06:50)', timestampEnd: '' },
]);
assert.equal(state.schichten.get(schichtId).ende, null);
});
test('beendeArbeitstag, updateArbeitstag und deleteArbeitstag', async t => {
const { connector, requests, state } = await startConnector(t);
const { ref } = await connector.beginneArbeitstag(3309, new Date('2026-10-14T05:00:00Z'));
const schichtId = Number(ref.split(':').at(-1));
await connector.beendeArbeitstag(ref, new Date('2026-10-14T15:30:00Z'));
await connector.updateArbeitstag(ref, { start: new Date('2026-10-14T04:45:00Z'), ende: null });
assert.deepEqual(posts(requests, '/module/arbeitszeit/api/updateSchicht.php').map(r => r.body), [
{ shiftID: schichtId, timestampStart: '14.10.26 (07:00)', timestampEnd: '14.10.26 (17:30)' },
{ shiftID: schichtId, timestampStart: '14.10.26 (06:45)', timestampEnd: '' },
]);
await assert.rejects(
() => connector.beendeArbeitstag(ref, new Date('2026-10-14T04:00:00Z')),
error => error instanceof ZeitbuchungError && error.grund === 'dauer'
);
await assert.rejects(() => connector.beendeArbeitstag(`verteilung:3309:2026-10-14:${schichtId}`, new Date()), /Arbeitstag-Referenz/);
await connector.deleteArbeitstag(ref);
assert.deepEqual(posts(requests, '/module/arbeitszeit/api/deleteSchicht.php')[0].body, {
shiftID: schichtId,
employeeID: 3309,
date: '2026-10-14',
});
assert.equal(state.schichten.get(schichtId).deleted, true);
await assert.rejects(() => connector.beendeArbeitstag(ref, new Date('2026-10-14T15:30:00Z')), /nicht gefunden/);
});