Compare commits
4
Commits
e6ce1d254b
...
659fdb5293
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
659fdb5293 | ||
|
|
d703e46125 | ||
|
|
6db6c5b278 | ||
|
|
d2c5db7e22 |
No files matched your search
@@ -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.
|
||||||
Vendored
+100
-1
@@ -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;
|
||||||
|
|||||||
Vendored
+1
-1
@@ -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
|
||||||
Vendored
+626
@@ -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,
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"AgrarmonitorConnector.js","sourceRoot":"","sources":["../src/AgrarmonitorConnector.ts"],"names":[],"mappings":";;;;;;AAAA,kDAA+F;AAC/F,qEAAkD;AAClD,iCAA8B;AAC9B,+CAAyC;AAqDzC,MAAa,qBAAqB;IAgBH;IAftB,IAAI,CAAiB;IAEpB,MAAM,CAAU,gBAAgB,GAAG,kEAAkE,CAAC;IAE7F,OAAO,CAAS;IAChB,UAAU,CAAS;IACnB,SAAS,CAAS;IAClB,SAAS,CAAU;IACnB,SAAS,CAAU;IACnB,aAAa,CAA4B;IACzC,MAAM,CAAU;IACzB,SAAS,CAAa;IACtB,OAAO,CAAiB;IACxB,eAAe,GAAyB,IAAI,CAAC;IAErD,YAA6B,OAAqC;QAArC,YAAO,GAAP,OAAO,CAA8B;QAChE,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,gCAAgC,CAAC;QACnE,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC,UAAU,IAAI,gCAAgC,CAAC,CAAC;QACnG,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,KAAK,CAAC;QAC5C,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,IAAI,CAAC;QAC3C,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,IAAI,CAAC;QAC3C,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,UAAU,CAAC;QACzD,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,IAAI;QACR,IAAI,CAAC,SAAS,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;QACvD,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACpC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,mBAAmB,EAAE,CAAC;QAE1C,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACnB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,cAAc,EAAE,CAAC;YAE1C,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;YACrB,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACzB,OAAO,IAAI,CAAC,eAAe,CAAC;QAC9B,CAAC;QAED,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;YACtD,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;QAC9B,CAAC,CAAC,CAAC;QAEH,OAAO,IAAI,CAAC,eAAe,CAAC;IAC9B,CAAC;IAED,KAAK,CAAC,YAAY;QAChB,IAAI,CAAC,SAAS,GAAG,IAAI,wBAAS,EAAE,CAAC;QACjC,MAAM,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC;QACvC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACpC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,mBAAmB,EAAE,CAAC;IAC5C,CAAC;IAED,KAAK,CAAC,WAAW;QACf,MAAM,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACtD,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,GAAG,GAAG,IAAI,CAAC,OAAO;QACrC,OAAO,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC;IACnD,CAAC;IAED,KAAK,CAAC,mBAAmB;QACvB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE;YACxC,YAAY,EAAE,CAAC;YACf,cAAc,EAAE,MAAM,CAAC,EAAE,CAAC,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG;SACxD,CAAC,CAAC;QAEH,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QAEzB,MAAM,gBAAgB,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;QAC9D,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,kBAAkB,CAAC,gBAAgB,CAAC,CAAC;QAEhH,OAAO;YACL,cAAc,EAAE,CAAC,UAAU;YAC3B,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,UAAU;YACV,gBAAgB;YAChB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,OAAO,EAAE,MAAM,IAAI,CAAC,cAAc,EAAE;SACrC,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,gBAAgB;QACpB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE;YACxC,YAAY,EAAE,CAAC;YACf,cAAc,EAAE,MAAM,CAAC,EAAE,CAAC,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG;SACxD,CAAC,CAAC;QAEH,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QAEzB,MAAM,WAAW,GAAG,OAAO,QAAQ,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3E,MAAM,mBAAmB,GAAG,WAAW,CAAC,QAAQ,CAAC,0BAA0B,CAAC,CAAC;QAE7E,OAAO;YACL,WAAW,EAAE,CAAC,mBAAmB;YACjC,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,mBAAmB;YACnB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,OAAO,EAAE,MAAM,IAAI,CAAC,cAAc,EAAE;SACrC,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,cAAc,CAClB,YAAmD;QAEnD,MAAM,cAAc,GAAG,YAAY,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;QAC1D,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QAE1C,IAAI,CAAC,cAAc,IAAI,CAAC,MAAM,EAAE,CAAC;YAC/B,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;QAClE,CAAC;QAED,MAAM,qBAAqB,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;QACrE,MAAM,eAAe,GAAG,OAAO,qBAAqB,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QACzG,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,eAAe,EAAE,6BAA6B,CAAC,CAAC;QAEhF,MAAM,gBAAgB,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAC3C,iCAAiC,EACjC;YACE,KAAK,EAAE,cAAc;YACrB,IAAI,EAAE,MAAM;YACZ,KAAK;SACN,EACD;YACE,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;aACnC;YACD,cAAc,EAAE,MAAM,CAAC,EAAE,CAAC,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG;SACxD,CACF,CAAC;QAEF,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QAEzB,MAAM,OAAO,GAAG,gBAAgB,CAAC,MAAM,IAAI,GAAG,IAAI,gBAAgB,CAAC,MAAM,GAAG,GAAG,CAAC;QAEhF,OAAO;YACL,OAAO;YACP,MAAM,EAAE,gBAAgB,CAAC,MAAM;YAC/B,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,2BAA2B,CAAC,CAAC,CAAC,8BAA8B;YAC/E,IAAI,EAAE;gBACJ,cAAc;gBACd,MAAM;gBACN,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;aAC7B;YACD,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,OAAO,EAAE,MAAM,IAAI,CAAC,cAAc,EAAE;SACrC,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,UAA6C,EAAE;QAClE,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,UAAU,CAAqB,SAAS,EAAE;YACpE,MAAM,EAAE;gBACN,QAAQ,EAAE,OAAO,CAAC,OAAO,IAAI,KAAK;aACnC;YACD,QAAQ,EAAE,OAAO,CAAC,QAAQ;SAC3B,CAAC,CAAC;QAEH,MAAM,YAAY,GAAG,QAAQ,CAAC,IAAI,CAAC;QAEnC,IAAI,CAAC,YAAY,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC;YACvD,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;QACzD,CAAC;QAED,OAAO,YAAY,CAAC,IAAiC,CAAC;IACxD,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,UAA6C,EAAE;QAC9D,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC;QACxD,OAAO,SAAS,CAAC,GAAG,CALine truncated
|
{"version":3,"file":"AgrarmonitorConnector.js","sourceRoot":"","sources":["../src/AgrarmonitorConnector.ts"],"names":[],"mappings":"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 truncated
|
||||||
Vendored
+12
@@ -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
|
||||||
Vendored
+1
@@ -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"}
|
||||||
Vendored
+21
@@ -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
|
||||||
Vendored
+1
@@ -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"}
|
||||||
Vendored
+2
-1
@@ -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
|
||||||
Vendored
+1
-1
@@ -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"}
|
||||||
Vendored
+3
-1
@@ -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
|
||||||
Vendored
+1
-1
@@ -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"}
|
||||||
Vendored
+65
@@ -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
|
||||||
Vendored
+1
-1
@@ -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
|
||||||
@@ -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×tamp_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.
|
||||||
@@ -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({
|
||||||
|
|||||||
@@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
@@ -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;
|
||||||
@@ -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"> </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);
|
||||||
|
});
|
||||||
@@ -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/);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user