Die Reporttabelle lief mit table-auto: ein langer Projektname wie
"HAYS - Globales Template und Implementierung: ... _Fresenius -
01.10.2025-31.07.2026" zog die Namensspalte auf und schob Stunden und
Betrag aus dem sichtbaren Bereich. Das truncate am Namen half nicht,
weil die Spalte keine Breite hatte, an der es haette greifen koennen.
Die Tabelle steht jetzt auf table-fixed mit festen Breiten fuer die
Zahlenspalten; der Name bekommt den Rest und bricht um (break-words).
Die Zahlen selbst bleiben einzeilig.
Dazu die Kennzahlenkarte: ihre vier Spalten mit den grossen Betraegen
passten ab lg nicht mehr in die Breite, der rechte Rand wurde von
overflow-x-hidden abgeschnitten. Sie bricht jetzt auf zwei Spalten um
und zeigt vier erst ab xl.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Im Zeitreport stehen interne Projekte und Abwesenheiten zwischen den
Kundenzahlen: 2025 sind das 19.451 von 45.762 Stunden, und die Auslastung
zeigt 146 Prozent, weil Urlaub und Krankheit mitzaehlen.
Ein Haken neben der Billable-Aufteilung blendet Projekte vom Typ
"nicht abrechenbar" aus. Er wirkt auf den ganzen Report - Summen,
Auslastung, alle vier Gruppierungen - und auf die Exporte, die denselben
Report bauen; im PDF-Kopf steht dann "Nicht abrechenbare Projekte:
ausgeblendet". Gefiltert wird in loadEntries, damit Kunden-, Projekt- und
Taskliste dieselbe Grundmenge sehen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Beim Nachziehen der Harvest-Historie sind zwei Regeln im Weg gestanden,
die fuer eine Uebernahme nicht passen:
- Beim Anlegen verlangte die API auch fuer nicht abrechenbare Projekte
einen Satz, obwohl validateProject ihn direkt danach verwirft. Nur
Abruf rechnet ueber den Satz am Projekt ab, also gilt die Pflicht auch
nur dort.
- Der Importweg liess hoechstens 24 Stunden je Tag zu. In Harvest stehen
Sammelbuchungen ("Nachbuchung DORA September und Oktober", 80 Stunden
auf einem Tag). Sie abzulehnen verliert die Stunden und verfaelscht das
Restbudget, sie zu verteilen erfindet Leistungsdaten - deshalb kommen
sie so herein, wie sie gebucht wurden. Eine Buchung darf jetzt bis zu
31 Tage umfassen, damit Zahlendreher weiter auffallen. Im
Buchungsdialog bleibt die Tagesgrenze bestehen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Die Uebernahme der Harvest-Historie ist an drei Stellen haengen geblieben:
- resolvePot hat abgelaufene Toepfe grundsaetzlich gesperrt. Fuer eine
historische Buchung ist das falsch - dort zaehlt, ob der Topf am
Leistungstag lief. Der Importweg setzt jetzt `historic`, das Volumen
wird weiter geprueft, damit Ueberbuchungen auffallen.
- In der SSO-Verwaltung fehlte die Ablagezone fuer die Rolle "Externe";
Konten liessen sich dort nicht zuordnen.
- In buildProjectDetail und der Kundenauswertung stand ein Rest der
Auslastungsaenderung (bisHeute/capTo) ohne Bezug im Scope. Beide
Reportseiten liefen dadurch auf 500.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Auslastung
Verglichen werden jetzt die bis heute gebuchten Stunden mit der Kapazitaet
bis heute. Die Kapazitaet endete schon immer am heutigen Tag; wurde im
Voraus gebucht - eine ganze Woche Urlaub am Montag - standen 40 Stunden
gegen 8 Stunden Kapazitaet und die Wochenansicht zeigte 500 Prozent. Die
Spalte "Stunden" zeigt weiter den ganzen Zeitraum.
Tasks
Das Feld zum Hinzufuegen lief aus der Karte heraus: die Zeile darf jetzt
umbrechen, und die Breitenangaben tragen "!", weil inputClass w-full
mitbringt und sonst gewinnt. Der Satz eines Tasks steht als schmales Feld
direkt in der Zeile und wird beim Verlassen gespeichert - vorher lag er
hinter einem Abfragedialog und war praktisch nicht zu finden.
Export der Buchungen
Kunde und Projekt kommen nur noch aus den eigenen Zuweisungen, auch fuer
Vorgesetzte. Eine Liste aller Projekte des Hauses war dort nur Suchaufwand.
Budgettopf
Neues Feld Bestellnummer (optional). Es erscheint im Stundennachweis als
eigene Kopfzeile, wenn die gebuchten Toepfe eine tragen.
Rolle "Externe"
Gleiche Rechte wie "Mitarbeiter" und derselbe Rang, die Rolle trennt nur,
wer zum Haus gehoert. Sie setzt is_external automatisch, damit Liste und
Kennzeichnung nicht widersprechen. Vorgesetzte duerfen sie vergeben.
Stellen, die bisher role === 'user' pruefen, fragen nun hasRole(lead) ab -
sonst haetten Externe alle Projekte gesehen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Urlaub, Krankheit, Elternzeit und Feiertage stehen in HRworks. Pulse holt
sie einmal taeglich und bucht sie auf das nicht abrechenbare Projekt
"Abwesenheiten". Nach HRworks geht nichts zurueck, es wird nur gelesen.
Die Stunden rechnet HRworks selbst: available-working-hours liefert je
Person und Tag das Soll und die verfuegbaren Stunden, die Differenz sind
die Abwesenheitsstunden. Damit stimmen Teilzeit und halbe Tage ohne
eigenen Kalender. relatedEvents nennt den Auslöser; Vorrang hat der
Feiertag, dann Krankheit, dann Abwesenheit.
Abgesprochene Regeln: nur genehmigte Abwesenheiten (validatedOk), bei
Krankmeldungen auch offene Bescheinigungen; nur bis heute buchen, der
Rest waechst taeglich nach; halbe Tage feste 4 Stunden; jeder aktive
Mitarbeiter wird dem Projekt zugewiesen, auch neue.
Jede Buchung traegt hrworks_ref = "<Nummer>@<Tag>" (unique). Ein zweiter
Lauf aendert nichts, Stornos und Verschiebungen verschwinden im naechsten
Lauf, DELETE /api/hrworks/entries entfernt genau diese Buchungen.
Nicht abrechenbare Projekte brauchen keinen Budgettopf mehr. Ein Topf
traegt Volumen, Laufzeit und Satz - das hat nur bei Billable und
Festpreis Sinn. Am Abwesenheitsprojekt war er nur ein Ablaufdatum, das
irgendwann Buchungen blockiert haette.
Taeglicher Lauf ueber systemd (perinco-pulse-hrworks.timer, 5:30 Uhr
Berliner Zeit, Persistent). Bedienung unter Einstellungen ->
Abwesenheiten (HRworks) mit Verbindungstest und Probelauf; der geheime
Schluessel liegt AES-verschluesselt in der Datenbank.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Der Dialog war 1152 Punkte breit, die Tabelle brauchte mehr - "Billable
Zeit" und "Non-billable Zeit" lagen ausserhalb und waren nur durch
seitliches Scrollen zu erreichen, was niemand sieht.
- Neue Dialogbreite 3xl: bis 1600 Punkte, hoechstens 95 % des Fensters.
- Die Tabelle liegt jetzt auf festen Spaltenbreiten (table-fixed): Datum,
Mitarbeiter und die beiden Zeitspalten bekommen ihre Breite, Kunde und
Projekt einen Anteil, den Rest nimmt der Task.
- Lange Kunden-, Projekt- und Tasknamen samt Notiz brechen um statt
abgeschnitten zu werden; die Zahlen brechen nie.
Geprueft bei 1920, 1440 und 1280 Punkten: Tabelle passt ohne Querscrollen,
die letzte Spalte liegt im Dialog. Auf dem Handy scrollt die Tabelle wie
vorher innerhalb des Dialogs, die Seite selbst nicht.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Satz je Task
Ein Task kann einen eigenen Satz tragen; leer bedeutet weiter "Satz des
Budgettopfs" bzw. bei Abruf "Satz des Projekts". Damit lassen sich
"remote" und "vor Ort" im selben Projekt abrechnen, ohne dafuer getrennte
Projekte anzulegen.
Das Budget bleibt in Stunden - gemischte Saetze aendern nichts daran, wann
ein Topf voll ist. Der Umsatz wird je Task-Satz aufsummiert: loadProjects
gruppiert den Topfverbrauch nach Task-Satz und gibt das Ergebnis an
potStats, Abruf rechnet je Task. Mitarbeiter sehen keine Saetze, auch
nicht die am Task.
Import
POST /api/imports/entries (nur Administrator) schreibt Buchungen fuer
beliebige Personen - POST /api/entries bucht bewusst nur auf den
angemeldeten Benutzer. Der Import laeuft durch dieselben Regeln: Projekt
aktiv, Person zugewiesen, Topf passend zum Datum, maximal 24 h am Tag.
dry_run schreibt nichts.
Jede uebernommene Buchung traegt ihre harvest_id (unique). Ein zweiter
Lauf legt nichts doppelt an. DELETE /api/imports/entries entfernt genau
die uebernommenen Zeiten; handgetippte bleiben unberuehrt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Die Summenzeile des Stundennachweises trug ihre Beschriftung in der Spalte
"Tag". Die ist rund 40 Punkt breit, also brach "Summe" dort in drei Zeilen
um ("Su/mm/e"), waehrend die Zahlen rechts daneben in einer Zeile standen.
drawRow laesst in der Summenzeile eine Beschriftung jetzt in die leeren
Spalten dahinter laufen und setzt sie einzeilig. Gefuellte Spalten werden
nicht einverleibt, die Zahlen bleiben also an ihrem Platz. Das gilt fuer
alle PDF-Tabellen, nicht nur den Stundennachweis; Excel und CSV waren nie
betroffen.
Rueckbau: diesen Commit reverten - die Aenderung steckt allein in
export/render.js.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Jeder setzt sein Bild unter "Mein Konto", Vorgesetzte und Administratoren
auch fuer die Benutzer, die sie verwalten. Wo bisher die Initialen standen,
steht jetzt das Bild - Seitenleiste, Kopfzeile, Team, Dashboard, Reports,
Projekte, Kunden und die Buchungsliste. Ohne Bild bleiben die Initialen.
Der Browser verkleinert das Bild vor dem Hochladen auf ein Quadrat von
256 px und schneidet mittig zu (lib/imageCompress.ts): aus einem 4-MB-Foto
werden wenige Kilobyte, und der Server muss kein Bild verarbeiten.
Gespeichert wird es als Data-URL in pulse_users.avatar (LONGTEXT). Kein
Upload-Verzeichnis, das gesichert und beim Deployment am Leben gehalten
werden muesste.
Geprueft wird serverseitig in lib/avatar.js: nur PNG, JPEG, GIF, WebP und
AVIF als Data-URL, hoechstens 2 MB. SVG ist bewusst nicht erlaubt, es kann
Skripte tragen. Externe http(s)-Adressen ebenso wenig - sonst laedt jeder
Seitenaufruf ein Bild von einem fremden Server und verraet, wer wann mit
Pulse arbeitet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ersetzt das perinco-Punktzeichen im Reiter durch ein gruenes P.
- favicon-p.svg: gezeichnete Form statt gesetzter Schrift, damit sie auf
jedem Rechner gleich aussieht und in jeder Groesse scharf bleibt. Auf
dunklen Reiterleisten schaltet sie per prefers-color-scheme in ein
helleres Gruen.
- favicon-p-32.png und favicon-p-180.png als Rueckfall fuer Browser ohne
SVG-Symbol, apple-touch-icon-p.png mit weisser Flaeche, weil iOS
durchsichtige Symbole auf Schwarz legt.
Eigene Dateinamen statt favicon.png: Apache laesst Bilder ein Jahr im
Cache liegen, unter gleichem Namen saehe man das neue Symbol sonst erst
nach einem harten Neuladen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Im Theme "Pixel Art" laeuft eine kleine Figur ueber die Steinmauer der
Seitenleiste, macht einen Huepfer und verschwindet wieder nach links.
Auftritt alle 60 Sekunden, der erste nach 6 Sekunden.
Die Figur ist eine eigene Zeichnung - gruene Muetze, Schutzbrille,
Latzhose - und steht als Pixelraster 14 x 16 direkt im Code, ohne
Bilddatei. Bewusst kein Sprite aus einem kommerziellen Spiel: solche
Figuren und Grafiken sind geschuetzt und gehoeren nicht in eine
produktiv genutzte Anwendung.
Technik wie bei den Wetter-Ebenen: ein Canvas genau ueber der
Seitenleiste, ohne Klickfang, unter den Dialogen, per ResizeObserver an
die ein- und ausklappbare Breite gebunden. Zwischen zwei Auftritten
laeuft ein Timer statt eines Animationsframes. Bei "Bewegung reduzieren"
und unterhalb der Tabletbreite bleibt alles aus.
Rueckbau: diesen Commit reverten, oder die Datei loeschen und die eine
Zeile in lib/theme.tsx entfernen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Jede Kundenueberschrift begann eine neue Rasterzeile. Bei einem Projekt je
Kunde stand dadurch eine Kachel allein in der Zeile und rechts blieb der
Platz leer - auf der Produktion mit fuenf Kunden untereinander sichtbar.
Die Kacheln laufen jetzt in einem Raster, sortiert nach Kunde und Projekt.
Der Kundenname steht dafuer neben dem Projektcode auf der Kachel selbst.
Die Detailansicht steht wie in der Liste unter der Uebersicht.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Umschalter Karten/Liste neben dem Statusfilter, die Wahl merkt sich der
Browser. Die Liste zeigt alle gefilterten Projekte flach ueber die Kunden
hinweg und sortierbar: Projekt mit Code, Toepfen und Tasks, Kunde, Typ,
gebuchte Stunden und Tage, Budget mit freiem Rest, Verbrauchsbalken,
offener Betrag mit erbrachtem Umsatz und das Team. Beträge nur ab
Vorgesetztem.
Ein Klick auf eine Zeile oeffnet dieselbe Detailansicht wie eine Kachel,
in der Liste unterhalb der Tabelle. Dafuer ist der Detailblock einmal
aufgebaut und wird in beiden Ansichten verwendet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Die Einblendanimationen laufen mit animation-fill-mode: both. Danach behaelt
das Element den Wert des letzten Keyframes, bei transform also eine
Einheitsmatrix statt "none". Das genuegt, um einen Bezugsrahmen fuer
position: fixed zu erzeugen: Ein Dialog unterhalb einer solchen Karte hat
sein Overlay nur noch ueber der Karte, deren overflow: hidden schneidet ihn
ab. Sichtbar beim Bearbeiten eines Budgettopfs in der Projektansicht - das
Fenster klebte an der Detailkarte und die Schaltflaechen waren abgeschnitten.
Modal und ConfirmDialog rendern jetzt per createPortal in document.body.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Neue Komponente EntryList zeigt einzelne Buchungen als sortierbare Tabelle
mit Datum, Kunde, Projekt, Task und der Zeit getrennt nach billable und
non-billable, inklusive Summen.
- Buchung: Umschalter Kalender/Liste neben Woche/Monat/Jahr, Auswahl wird
im Browser gemerkt. Klick auf eine Zeile oeffnet die Buchung.
- Reports: Klick auf einen Mitarbeiter oeffnet dessen Buchungen des
Reportzeitraums als Liste; ein gesetzter Kundenfilter wird uebernommen.
- Kunden: Umschalter Karten/Liste mit Ort, Projekten, Stunden und Umsatz.
- GET /api/entries akzeptiert dafuer customer_id, Segmented nimmt Icons,
Modal kennt die Groesse 2xl.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Abruf braucht keinen Budgettopf: es gibt kein zugesagtes Volumen und kein
Ablaufdatum, gebucht wird, was anfaellt. Der Satz kann deshalb nicht am
Topf haengen und steht am Projekt (pulse_projects.rate, Stunde oder Tag).
Der Umsatz sind die abrechenbaren Stunden mal diesem Satz; offen ist
nichts.
- Migration ergaenzt den ENUM-Wert und die Spalte rate
- Anlegen verlangt einen Satz und nimmt keine Toepfe an; ein Topf
nachtraeglich anzulegen wird abgewiesen
- Wechsel auf Abruf nur, solange auf den Toepfen nichts gebucht ist -
sonst wuerden Buchungen ihr Budget verlieren; leere Toepfe fallen weg
- Buchungsdialog, Projektkarte, Projektdetail und Projektreport zeigen
Satz und Umsatz statt Budgetbalken
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Das Logo lag in public/ und hatte immer denselben Namen. Apache gibt
Bildern ein Jahr Cache-Zeit mit - eine neue Fassung war deshalb erst
nach hartem Neuladen zu sehen, obwohl der Server sie schon auslieferte.
Jetzt liegt es in src/assets/ und wird importiert: Vite haengt einen Hash
an den Dateinamen (assets/pulse-logo-<hash>.png), der sich bei jeder
Aenderung mitaendert.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/Pulse-Logo.png deckte nur die erste Vorlage ab, Pulse-Logo2.png landete
dadurch im Repo. Jetzt greift /Pulse-Logo*.png.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pulse-Logo2.png war bereits freigestellt, deshalb nur zugeschnitten und
auf 160 px Hoehe skaliert (73 KB). Angezeigt 46 px hoch, 159 px breit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Das groessere Logo machte die Logo-Zeile 79 px hoch, der Header blieb bei
71,5 px - die Linie lief schief. Beide haengen jetzt an
--app-bar-height (4,5 rem); das Logo begrenzt nur seine maximale Hoehe.
So bleibt es buendig, auch wenn Logo oder Titel sich aendern.
Dazu die neu zugeschnittene Logovorlage uebernommen: weniger Freiflaeche,
freigestellt und auf 160 px Hoehe skaliert.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Oben links steht jetzt das Pulse-Logo statt des perinco-Logos; der
Schriftzug "Pulse" daneben ist damit doppelt und entfaellt. Das Logo ist
freigestellt (grauer Grund und Bildrauschen entfernt, auf 140 px Hoehe
zugeschnitten) und wird im Dunkelmodus nicht umgefaerbt - die Wuerfel
sind gruen und sollen gruen bleiben.
Die schaukelnden Huegel im hellen Aura lagen als Hintergrundbild und
wurden ueber background-position bewegt; bei diesen breiten Formen war
das nicht zu sehen. Sie liegen nun in einer eigenen Ebene und wandern
per transform 26 px hin und her, dazu 5 px auf und ab.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Uebersicht der sieben Farbwelten mit ihren Animationen, dazu die vier
Stellen, die ein neues Theme braucht - die Pruefung im Backend wurde
beim Nachziehen von Rainy, Snow und Aura dreimal vergessen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Erste Fassung, live unter https://pulse.perinco.de.
Frontend React 19 + TypeScript + Tailwind 4 (Vite), Backend Express mit
mysql2 auf MariaDB, Exporte als PDF (pdfkit), Excel (exceljs) und CSV.
Aufbau, Designsystem und die sieben Themes kommen aus dem perinco CRM.
Fachlich:
- Buchung mit Wochen-, Monats- und Jahresansicht, Zeiten als 1:23 oder 1,5
- Projekte mit n Budgettoepfen: Laufzeit, Volumen, Sperre bei Ablauf oder
aufgebrauchtem Budget, Buchung nur auf den passenden Topf
- Kunden, Team mit Hierarchie, Reports im Harvest-Stil, Monatsexporte
- Rollen user/lead/management/admin, doppelt geprueft (Menue und Backend)
- SSO gegen Microsoft Entra ID, Uebernahme der AD-Benutzer per Drag & Drop
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>