@patimweb/pi-zep 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/package.json +1 -1
- package/src/clients/zep-client.ts +2 -2
- package/src/clients/zep-parse.ts +67 -22
- package/src/formatting/formatters.ts +2 -1
- package/src/tools/zep-book.ts +12 -4
- package/src/types.ts +7 -0
package/README.md
CHANGED
|
@@ -107,17 +107,17 @@ Other message texts observed:
|
|
|
107
107
|
```
|
|
108
108
|
zep_setup name=work userid=patrick.weppelmann password=… mandant=zepneoimpulse
|
|
109
109
|
zep_status probe=true
|
|
110
|
-
zep_projects projekt=
|
|
110
|
+
zep_projects projekt=Contoso
|
|
111
111
|
zep_week
|
|
112
|
-
zep_book datum=2026-09-17 von=08:00 bis=12:00 projekt=413 vorgang=90-01 taetigkeit=co bemerkung="
|
|
112
|
+
zep_book datum=2026-09-17 von=08:00 bis=12:00 projekt=413 vorgang=90-01 taetigkeit=co bemerkung="4711 Beispiel-Ticket"
|
|
113
113
|
zep_book datum=2026-09-17 entries=[{von:"08:00",bis:"12:00",bemerkung:"…"},{von:"13:00",bis:"17:00",bemerkung:"…"}]
|
|
114
114
|
```
|
|
115
115
|
|
|
116
116
|
`projekt` and `vorgang` accept either an id (`413`, `3284`) or part of the label
|
|
117
|
-
(`
|
|
117
|
+
(`Contoso`, `90-01`). A full example, `dryRun` first:
|
|
118
118
|
|
|
119
119
|
```
|
|
120
|
-
zep_book projekt=
|
|
120
|
+
zep_book projekt=Contoso vorgang=90-01 datum=2026-09-17 von=08:00 bis=12:00 dryRun=true
|
|
121
121
|
```
|
|
122
122
|
|
|
123
123
|
## Important behaviour
|
package/package.json
CHANGED
|
@@ -491,8 +491,8 @@ export class ZepClient {
|
|
|
491
491
|
letzterReiseOrt: "D-Office",
|
|
492
492
|
ortProjektrelevant: "1",
|
|
493
493
|
reise: "0",
|
|
494
|
-
startort: "
|
|
495
|
-
zielort: "
|
|
494
|
+
startort: "Beispielweg 1",
|
|
495
|
+
zielort: "Beispielort",
|
|
496
496
|
fahrzeug: "Firmenwagen",
|
|
497
497
|
km: "285",
|
|
498
498
|
mitfahrer: "1",
|
package/src/clients/zep-parse.ts
CHANGED
|
@@ -297,13 +297,51 @@ function resolveDayDate(dayLabel: string, kwDate: string): string | null {
|
|
|
297
297
|
return monday.toISOString().slice(0, 10);
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
/**
|
|
301
|
+
* Label of a day row: `Do 17.09.`.
|
|
302
|
+
*
|
|
303
|
+
* The label cell also carries the meals/breaks icons, whose text content is
|
|
304
|
+
* literally `restaurant` / `local_cafe`; taking the plain text of the cell
|
|
305
|
+
* would render as `Do 17.09. restaurant`. Prefer the dedicated weekday and
|
|
306
|
+
* day/month divs and only fall back to text surgery.
|
|
307
|
+
*/
|
|
308
|
+
function dayRowLabel(cellHtml: string, joined: string): string {
|
|
309
|
+
const weekday = /class="modimidofrsaso"[^>]*>\s*([A-Za-z]{2})\s*</.exec(cellHtml)?.[1];
|
|
310
|
+
const dayMonth = /class="tag_monat"[^>]*>\s*([0-9]{2}\.[0-9]{2}\.)\s*</.exec(cellHtml)?.[1];
|
|
311
|
+
if (weekday && dayMonth) return `${weekday} ${dayMonth}`;
|
|
312
|
+
|
|
313
|
+
return joined
|
|
314
|
+
.split(/\s{2,}/)[0]!
|
|
315
|
+
.replace(/\s*(restaurant|local_cafe)\b.*$/i, "")
|
|
316
|
+
.trim();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Place of work of a booking row, or null when ZEP does not render it.
|
|
321
|
+
*
|
|
322
|
+
* The Ort cell is the row's last column (`von` + 9) and, unlike the other
|
|
323
|
+
* metadata columns, it carries no `data-columnid` of its own. ZEP only emits
|
|
324
|
+
* it when there is something to report, so an absent cell means the booking
|
|
325
|
+
* sits on the default place of work (`NULL`). A trailing `*` marks a
|
|
326
|
+
* deviation from that default and is not part of the value.
|
|
327
|
+
*/
|
|
328
|
+
function bookingOrt(flat: ReadonlyArray<string>, from: number): string | null {
|
|
329
|
+
return (flat[from + 9] ?? "").replace(/[\s*]+$/, "").trim() || null;
|
|
330
|
+
}
|
|
331
|
+
|
|
300
332
|
/**
|
|
301
333
|
* Parse the week table rendered into `#ProjektzeitTableDiv`.
|
|
302
334
|
*
|
|
303
|
-
* Row shapes observed on the live system:
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
335
|
+
* Row shapes observed on the live system (2026-09-17, ZEP v7.13.88):
|
|
336
|
+
*
|
|
337
|
+
* day, no bookings `["Do 17.09. restaurant", ""]`
|
|
338
|
+
* day, with bookings `["Do 17.09. restaurant", "", "editdelete", von, bis, Dauer, "", Projekt, Vorgang, Taet, fakturierbar, Bemerkung, Ort]`
|
|
339
|
+
* further bookings `["editdelete", von, bis, Dauer, "", Projekt, Vorgang, Taet, "payments", Bemerkung, Ort]`
|
|
340
|
+
* sum row `["Summe", "4,00", ...]`
|
|
341
|
+
*
|
|
342
|
+
* The day's FIRST booking is rendered inside the day row itself (the label
|
|
343
|
+
* cell then has `rowspan=N`), which is why the booking columns must be read
|
|
344
|
+
* relative to the first `HH:MM` cell rather than at a fixed offset.
|
|
307
345
|
*/
|
|
308
346
|
export function parseWeek(tableHtml: string, kwDate: string): ZepWeek {
|
|
309
347
|
const days: ZepWeekDay[] = [];
|
|
@@ -341,19 +379,22 @@ export function parseWeek(tableHtml: string, kwDate: string): ZepWeek {
|
|
|
341
379
|
// --- day header -------------------------------------------------------
|
|
342
380
|
const joined = flat.join(" ").trim();
|
|
343
381
|
const dayMatch = DAY_RE.exec(joined);
|
|
344
|
-
|
|
382
|
+
// A day row is identified by its `id="day_YYYY-MM-DD"` cell; the label
|
|
383
|
+
// text is only used as a fallback so that a markup change in the label
|
|
384
|
+
// cannot silently re-attach a day's bookings to the previous day.
|
|
385
|
+
const dayId =
|
|
386
|
+
/id="day_(\d{4}-\d{2}-\d{2})"/.exec(row.html) ??
|
|
387
|
+
/openMahlzeiten\s*\(\s*this\s*,\s*'(\d{4}-\d{2}-\d{2})'/.exec(row.html);
|
|
388
|
+
if (dayId || (dayMatch && cells.length <= 3)) {
|
|
345
389
|
flush();
|
|
346
|
-
const
|
|
347
|
-
/id="day_(\d{4}-\d{2}-\d{2})"/.exec(row.html) ??
|
|
348
|
-
/openMahlzeiten\s*\(\s*this\s*,\s*'(\d{4}-\d{2}-\d{2})'/.exec(row.html);
|
|
349
|
-
const label = joined.split(/\s{2,}/)[0] ?? joined;
|
|
390
|
+
const label = dayRowLabel(cells[0] ?? "", joined) || joined;
|
|
350
391
|
current = {
|
|
351
392
|
label,
|
|
352
|
-
date:
|
|
393
|
+
date: dayId?.[1] ?? resolveDayDate(label, kwDate),
|
|
353
394
|
bookings: [],
|
|
354
395
|
classes: row.classes,
|
|
355
396
|
};
|
|
356
|
-
continue
|
|
397
|
+
// deliberately no `continue`: this row may carry the day's first booking
|
|
357
398
|
}
|
|
358
399
|
|
|
359
400
|
if (!current) continue;
|
|
@@ -368,22 +409,26 @@ export function parseWeek(tableHtml: string, kwDate: string): ZepWeek {
|
|
|
368
409
|
}
|
|
369
410
|
|
|
370
411
|
// --- booking row ------------------------------------------------------
|
|
371
|
-
|
|
372
|
-
|
|
412
|
+
// Anchor on the `von` cell: cell 1 in a standalone booking row, cell 3
|
|
413
|
+
// behind the label in a day row that carries its first booking.
|
|
414
|
+
const from = flat.findIndex((c, index) => index > 0 && /^\d{2}:\d{2}$/.test(c));
|
|
415
|
+
if (from === -1) continue;
|
|
373
416
|
|
|
374
|
-
const objectId =
|
|
417
|
+
const objectId =
|
|
418
|
+
/objectId=?(\d+)/.exec(row.html)?.[1] ?? null;
|
|
375
419
|
current.bookings.push({
|
|
376
420
|
objectId,
|
|
377
421
|
dayLabel: current.label,
|
|
378
422
|
date: current.date,
|
|
379
|
-
from: flat[
|
|
380
|
-
to: flat[
|
|
381
|
-
duration: flat[
|
|
382
|
-
project: flat[
|
|
383
|
-
vorgang: flat[
|
|
384
|
-
taetigkeit: flat[
|
|
385
|
-
billable: /payments/i.test(flat[
|
|
386
|
-
comment: flat[
|
|
423
|
+
from: flat[from] ?? "",
|
|
424
|
+
to: flat[from + 1] ?? "",
|
|
425
|
+
duration: flat[from + 2] ?? "",
|
|
426
|
+
project: flat[from + 4] ?? "",
|
|
427
|
+
vorgang: flat[from + 5] ?? "",
|
|
428
|
+
taetigkeit: flat[from + 6] ?? "",
|
|
429
|
+
billable: /payments/i.test(flat[from + 7] ?? "") || /payments/i.test(row.html),
|
|
430
|
+
comment: flat[from + 8] ?? "",
|
|
431
|
+
ort: bookingOrt(flat, from),
|
|
387
432
|
});
|
|
388
433
|
}
|
|
389
434
|
|
|
@@ -65,7 +65,8 @@ export function formatWeek(week: ZepWeek): string {
|
|
|
65
65
|
lines.push(
|
|
66
66
|
` #${booking.objectId ?? "?"} ${booking.from}-${booking.to} (${booking.duration}) ` +
|
|
67
67
|
`${booking.project} / ${booking.vorgang} / ${booking.taetigkeit}` +
|
|
68
|
-
`${booking.billable ? " [billable]" : ""}
|
|
68
|
+
`${booking.billable ? " [billable]" : ""}` +
|
|
69
|
+
`${booking.ort ? ` @ ${booking.ort}` : ""}`,
|
|
69
70
|
);
|
|
70
71
|
if (booking.comment) lines.push(` ${booking.comment}`);
|
|
71
72
|
}
|
package/src/tools/zep-book.ts
CHANGED
|
@@ -64,13 +64,13 @@ export const ZepBookTool = {
|
|
|
64
64
|
bemerkung: Type.Optional(
|
|
65
65
|
Type.String({
|
|
66
66
|
description:
|
|
67
|
-
"Comment. Convention: include the ticket/story numbers (e.g. '
|
|
67
|
+
"Comment. Convention: include the ticket/story numbers (e.g. '4711 Ladeanzeige, 4712 Fach-HU-Nummer').",
|
|
68
68
|
}),
|
|
69
69
|
),
|
|
70
70
|
projekt: Type.Optional(
|
|
71
71
|
Type.String({
|
|
72
72
|
description:
|
|
73
|
-
"Project id (e.g. '413' for
|
|
73
|
+
"Project id (e.g. '413' for Contoso) or part of its label (e.g. 'Contoso'). Falls back to the project ZEP has selected for this user.",
|
|
74
74
|
}),
|
|
75
75
|
),
|
|
76
76
|
vorgang: Type.Optional(
|
|
@@ -85,7 +85,10 @@ export const ZepBookTool = {
|
|
|
85
85
|
}),
|
|
86
86
|
),
|
|
87
87
|
ort: Type.Optional(
|
|
88
|
-
Type.String({
|
|
88
|
+
Type.String({
|
|
89
|
+
description:
|
|
90
|
+
"Location code, e.g. 'D-Office' (office), 'D' (inland away) or 'D-3M'. The default 'NULL' is the first place of work (Erste Tätigkeitsstätte).",
|
|
91
|
+
}),
|
|
89
92
|
),
|
|
90
93
|
entries: Type.Optional(
|
|
91
94
|
Type.Array(
|
|
@@ -228,6 +231,10 @@ export const ZepBookTool = {
|
|
|
228
231
|
const comment = entry.bemerkung ?? params.bemerkung ?? "";
|
|
229
232
|
|
|
230
233
|
const vorgangLabel = scope.vorgaenge.find((v) => v.id === vorgangId)?.label;
|
|
234
|
+
// Always state the place of work that goes onto the wire: 'NULL' is
|
|
235
|
+
// the default (Erste Tätigkeitsstätte) and is exactly what a booking
|
|
236
|
+
// without an explicit ort gets, so it must not be invisible here.
|
|
237
|
+
const ortLabel = form.orte.find((o) => o.id === ort)?.label;
|
|
231
238
|
|
|
232
239
|
planned.push({
|
|
233
240
|
request: {
|
|
@@ -245,7 +252,8 @@ export const ZepBookTool = {
|
|
|
245
252
|
`${date} ${from}-${to} (${duration})\n` +
|
|
246
253
|
` project ${projectId}${scope.project ? ` "${scope.project.label}"` : ""}\n` +
|
|
247
254
|
` Vorgang ${vorgangId}${vorgangLabel ? ` "${vorgangLabel}"` : ""}\n` +
|
|
248
|
-
` Tätigkeit ${taetigkeit}
|
|
255
|
+
` Tätigkeit ${taetigkeit}, Ort ${ort}` +
|
|
256
|
+
`${ortLabel && ortLabel !== ort ? ` (${ortLabel})` : ""}` +
|
|
249
257
|
`${comment ? `\n "${comment}"` : ""}`,
|
|
250
258
|
});
|
|
251
259
|
}
|
package/src/types.ts
CHANGED
|
@@ -94,6 +94,13 @@ export interface ZepBooking {
|
|
|
94
94
|
readonly taetigkeit: string;
|
|
95
95
|
readonly billable: boolean;
|
|
96
96
|
readonly comment: string;
|
|
97
|
+
/**
|
|
98
|
+
* Place of work as the table renders it, e.g. `D-Office`.
|
|
99
|
+
*
|
|
100
|
+
* ZEP only draws this cell when there is something to say, so `null` means
|
|
101
|
+
* "no cell rendered" and therefore the default place of work (`NULL`).
|
|
102
|
+
*/
|
|
103
|
+
readonly ort: string | null;
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
export interface ZepWeekDay {
|