Lalabase
티켓·문서·근무시간·고객대화를 한 곳에 모아 KI로 검색, 소프트웨어 기업 고객 프로젝트용, 오픈소스로 자체 서버·독일 클라우드에 배포
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Sechs kleine PWAs für den Büroalltag, alle im gleichen Repo, alle einzeln als App auf dem Homescreen installierbar: • Zeiterfassung — Zeit pro Projekt erfassen, Sync auf Nextcloud. • Quittung — Belege fotografieren/hochladen, für Nextcloud + Banana aufbereiten. • Wettbewerbsprogramme — Architekturwettbewerbe (JSON) hochladen und vergleichen. • Offerten — Offerten aus Modulen zusammenstellen, Kosten berechnen, auf Nextcloud sichern. • Adressliste — Adressen filtern, Ansichten speichern, auf Nextcloud sichern. • Pendenzen — To-do-Liste nach Projekt und Person filterbar, auf Nextcloud sichern.README 전문 보기 ↗
Acht kleine PWAs für den Büroalltag, alle im gleichen Repo, alle einzeln als App auf dem Homescreen installierbar:
Die Startseite (index.html im Repo-Root) ist nur ein Launcher: ein
App-Icon-Raster wie auf dem Smartphone-Homescreen (Icon + kurzes Label,
Beschreibung nur noch als Tooltip) statt einer langen Liste mit
Beschreibungstext. Jede App hat ihr eigenes manifest.json und ihren eigenen
service-worker.js — man kann also entweder die Startseite installieren
(Icon-Raster) oder direkt auf einer Unterseite "Zum Home-Bildschirm
hinzufügen" tippen, dann landet nur diese eine App als eigenes Icon auf dem
Homescreen.
Gemeinsamer Code (Nextcloud-Login, WebDAV-Zugriff, Einstellungen-Dialog,
zentrale Konfiguration) liegt in shared/common.js und wird von allen Apps
eingebunden. localStorage ist pro Domain (nicht pro Unterordner) gültig —
einmal in irgendeiner App unter dem Zahnrad-Symbol eingerichtet, gelten
Benutzername/App-Passwort automatisch auch in den anderen.
Kein eigenes Anzeigename-Feld (mehr) — der Name, der z.B. in der
Zeiterfassungs-CSV als "Person" erscheint, wird automatisch aus dem
Benutzernamen abgeleitet (deriveDisplayName() in shared/common.js): bei
einer E-Mail-Adresse der Teil vor dem "@", sonst der ganze Benutzername,
erster Buchstabe gross (z.B. max → "Max", [email protected] → "Max").
Sicherheitshinweis: Das App-Passwort wird ausschliesslich lokal im Browser gespeichert (localStorage) und niemals ins Repo committet.
Jede Person macht das auf ihrem eigenen Gerät mit ihrem eigenen Login — jeder Account hat seinen eigenen, komplett getrennten Nextcloud-Dateibereich.
Die Managed Nextcloud bei hosting.de schickt bei Cross-Origin-Requests
(Browser → Nextcloud von einer anderen Domain aus) keine
Access-Control-Allow-Origin-Header — der Browser blockiert deshalb den
direkten Zugriff. Deshalb sprechen alle Apps nicht direkt mit Nextcloud,
sondern über einen kleinen Cloudflare Worker als Proxy (worker.js,
gemeinsam für alle Apps). Der Worker läuft server-seitig, hat also kein
CORS-Problem beim Weiterleiten, und ergänzt in der Antwort die fehlenden
Header. Er speichert nichts — die Daten liegen weiterhin ausschliesslich auf
eurer eigenen Nextcloud.
zeit-proxy) → Deploy (legt erstmal einen
Platzhalter an).worker.js einfügen,
Deploy klicken.https://zeit-proxy.euer-name.workers.dev.PROXY_URL in shared/common.js eintragen (eine einzige
Stelle, gilt für alle Apps), committen, pushen.Falls der Worker schon läuft: Bei jeder Erweiterung der App (neue
HTTP-Methode, neuer Header) muss der Code in worker.js erneut im
Cloudflare-Dashboard eingefügt und deployed werden — Copy-Paste, kein
CLI-Tool nötig. Aktuell braucht der Worker GET, PUT, MKCOL, PROPFIND
und DELETE.
Falls der Support meldet, dass CORS-Header für die Nextcloud-Instanz
aktiviert wurden, könnten die Apps auch direkt gegen Nextcloud laufen (ohne
Worker) — das wäre ein kleiner Rückbau in shared/common.js. Bis dahin ist
der Worker die zuverlässigere Lösung.
Alle App-Daten liegen zentral gebündelt unter Buero/Admin/App/, ein
Unterordner pro Modul (appModuleFolderPath() in shared/common.js):
| Modul | Ordner |
|---|---|
| Adressliste | Buero/Admin/App/Adressen (Ansichten in .../Adressen/Ansichten) |
| Offerten | Buero/Admin/App/Offerten und Rechnungen |
| Pendenzen | Buero/Admin/App/Pendenzen |
| Protokoll | Buero/Admin/App/Protokolle |
| Zeiterfassung | Buero/Admin/App/Zeiterfassung |
| Wettbewerbsprogramme | Buero/Admin/App/Wettbewerbe |
| Timeline | Buero/Admin/App/Timeline |
| Finanzplanung | Buero/Admin/App/Finanzplanung (ein File pro Jahr) |
| Finanzen-Stammdaten (Kontenplan/Kategorien/MwSt, vertraulich) | Buero/Admin/App/Finanzen |
| Zentrale Konfiguration: Mitarbeitende + Büro-Name | Buero/Admin/App/config.json |
| Zentrale Konfiguration: Projekte | Buero/Admin/App/projekte.txt |
Ausnahme: die eigentlichen Quittungen/Rechnungsbelege (Fotos/PDFs) bleiben
unter Buero/Admin/Finanzen (ausserhalb von App/) — nur die vertraulichen
Stammdaten (Kontenplan/Kategorien/MwSt-Codes) liegen in App/Finanzen, siehe
Abschnitt "Quittung" unten. Die Unterschriften-Bilder für den PDF-Export
bleiben ebenfalls an ihrem bisherigen Ort (Buero/Admin/KLG und Rechtliches/ Unterschriften), da sie auch von anderen Workflows verwendet werden.
Mitarbeitende kommen zentral aus Buero/Admin/App/config.json, Projekte aus
Buero/Admin/App/projekte.txt, siehe Abschnitt "Zentrale Konfiguration"
unter "Zeiterfassung" unten.
Zeit per Knopfdruck erfassen (P1 / P2 / P3 / Pause / Stop), Einträge werden automatisch als CSV auf Nextcloud synchronisiert. Läuft offline und synchronisiert, sobald wieder Netz da ist.
Zielordner: TARGET_FOLDER_PATH in zeiterfassung/app.js (Standard
Buero/Admin/App/Zeiterfassung, siehe Abschnitt "Nextcloud-Ordnerstruktur"
oben), wird bei Bedarf automatisch angelegt.
config.json + projekte.txt)Mitarbeitende und Projekte werden zentral von der Büroleitung verwaltet, in
zwei getrennten Dateien auf Nextcloud — bewusst nicht in einer
gemeinsamen JSON-Datei, weil die Projektliste von beiden Personen oft
bearbeitet wird und eine reine Textdatei dafür praktischer ist (kein
JSON-Syntaxrisiko durch Kommas/Anführungszeichen). Alle Module laden beide
über dieselbe Funktion (refreshAppConfig() in shared/common.js) — beim
Start (nach dem Login), nach dem Speichern der Einstellungen sowie danach
alle 60 Sekunden/beim Zurückkehren in den Tab. Ein Login (Benutzer +
App-Passwort) ist dafür zwingend nötig — es gibt keinen öffentlichen,
zugangsdatenfreien Lesezugriff mehr. Beide Dateien werden unabhängig
voneinander geladen: ist eine (noch) nicht erreichbar, bleibt für die
jeweils andere trotzdem der letzte bekannte Stand erhalten.
Buero/Admin/App/config.json — Mitarbeitende + Büro-Name:
{
"bueroName": "Muster Architekten AG",
"mitarbeitende": [
{ "key": "max", "name": "Max Muster", "datei": "Unterschrift_Max_Muster.png" },
{ "key": "erika", "name": "Erika Muster", "datei": "Unterschrift_Erika_Muster.png" }
]
}
bueroName erscheint als Titel auf der Startseite (Dashboard) sowie im
Browser-Tab, anstelle des sonst fest hinterlegten "Büro" — fehlt das Feld
(oder ist noch nicht eingeloggt), bleibt es beim generischen "Büro" als
Fallback. key (interner Schlüssel, z.B. für Kürzel/Zuordnung), name
(Anzeigename), datei (Dateiname der Unterschrift auf Nextcloud, siehe
Abschnitt "Unterschriften" bei den Offerten — nur dort gebraucht, kann bei
anderen Einträgen weggelassen werden).
Buero/Admin/App/projekte.txt — Projekte, ein Eintrag pro Zeile,
optional mit dreistelliger Projektnummer, Leerschlag, Projekttitel (der
Titel darf selbst Leerschläge enthalten; eine Zeile ohne führende Nummer ist
ebenso gültig und besteht dann nur aus dem Titel):
021 Neubau Werkhof
014 Umbau Altstetten
003 Verwaltung
Zeiterfassung, Pendenzen und Timeline ordnen Einträge, filtern und weisen Farben über den Projektnamen zu, nicht über die Nummer — eine Nummer ist dort rein kosmetisch (wird beim Anzeigen aus der Zeile herausgeparst, taucht aber nirgends in der App auf) und darf durchaus mehrfach vergeben sein, z.B. für mehrere interne/nicht-projektbezogene Kategorien:
000 Büro Allgemein
000 Akquisition
"Büro Allgemein" und "Akquisition" bekommen hier trotz identischer Nummer
"000" unterschiedliche Farben und werden beim Filtern korrekt auseinander-
gehalten (Farbe = String-Hash des Namens, siehe stringHash() in
zeiterfassung/app.js/pendenzen/app.js/zeiterfassung/dashboard/js/ dashboard.js). Einzige Einschränkung: der Name muss eindeutig sein — zwei
Zeilen mit demselben Namen wären für Zeiterfassung/Pendenzen/Timeline nicht
unterscheidbar. Ein Projekt umzubenennen trennt bestehende Einträge vom
"neuen" Namen (kein stabiler ID-Bezug wie bei einer reinen Nummer) — dafür
sind Nummern-Dopplungen (s.o.) unproblematisch, was in der Praxis öfter
vorkommt als eine Umbenennung.
Protokoll und Rechnungen (bei den Offerten) hingegen wählen ein Projekt über ein Dropdown mit Nummer UND Name gemeinsam (siehe jeweiliger Abschnitt unten). Auch hier ist eine mehrfach vergebene Nummer unproblematisch: die Dropdown-Optionen selbst sind intern über ihre Position in der Liste (nicht über die Nummer) eindeutig identifizierbar, sodass sich zwei gleichnummerierte Projekte trotzdem sauber auseinanderhalten lassen — sowohl beim Auswählen als auch beim späteren Wiederöffnen eines gespeicherten Protokolls/einer Rechnung (die richtige Option bleibt vorausgewählt, nicht z.B. die erste mit derselben Nummer).
Später ändern: einfach die jeweilige Datei in Nextcloud bearbeiten und speichern — alle Geräte übernehmen die Änderung automatisch (spätestens nach 60s bzw. beim nächsten Tab-Wechsel), kein Code-Update nötig.
Pro Person und Jahr eine Datei (im jeweils eigenen Nextcloud-Account):
/Buero/Admin/Zeiterfassung/zeiterfassung_2026.csv
Spalten:
Datum,Projekt,Dauer_Min,Kommentar,Person
2026-08-13,Projekt Nord,105.30,Vormittag Konzept, Nachmittag Umsetzung,Max
2026-08-13,Projekt Süd,12.45,Kurzes Telefonat,Max
Es gibt eine Zeile pro Tag und Projekt, nicht eine Zeile pro Start/Stop-Wechsel — mehrere Wechsel zum selben Projekt am selben Tag werden zu einer Summe zusammengefasst. Beim Sync wird die passende Zeile gesucht und aktualisiert; nur wirklich neue Tag/Projekt-Kombinationen werden angehängt.
Dauer_Min ist auf 2 Nachkommastellen genau.Kommentar lässt sich in der App unter "Heute" pro Projekt eintragen,
bezieht sich auf die Tagessumme, nur für den heutigen Tag editierbar.Person kommt aus dem Anzeigename, der automatisch aus dem Benutzernamen
abgeleitet wird (siehe "Ersteinrichtung" oben).Eine separate, schreibgeschützte Auswertungsseite (Charts + Tabelle nach
Projekt/Person) liegt unter zeiterfassung/dashboard/ — liest die CSVs über
öffentliche Nextcloud-Freigabelinks (PERSON_SOURCES in
dashboard/js/dashboard.js), kein Login nötig. Von der Startseite aus über
ein eigenes Icon ("Auswertung") erreichbar.
Filterbar nach Jahr, Projekt, Person sowie zusätzlich nach Zeitraum: "Ganzes Jahr" (Standard), "Heute", "Diese Woche", "Dieser Monat" oder "Frei wählbar" (zwei Datumsfelder). Die Jahres-CSV wird weiterhin komplett geladen, der Zeitraum-Filter schränkt die schon geladenen Zeilen per Datumsvergleich zusätzlich ein. "Heute"/"Diese Woche"/"Dieser Monat" beziehen sich immer auf das echte heutige Datum — weicht das vom gerade gewählten Jahr ab, wechselt die Jahresauswahl automatisch mit (sonst gäbe es scheinbar keine Treffer). Ein frei gewählter Zeitraum, der über einen Jahreswechsel hinausgeht, wird nicht unterstützt — gezeigt wird dann nur der Teil im gerade geladenen Jahr.
Charts: "Nach Projekt" (horizontale Balken, ein Balken pro Projekt) und "Nach
Tag" (vertikale Balken nebeneinander, ein Balken pro Kalendertag im aktuell
gefilterten Zeitraum — auch Tage ohne Eintrag, als flacher, hell
eingefärbter Strich, damit Lücken im Diagramm sichtbar bleiben statt
stillschweigend zu fehlen; ohne Einzelbeschriftung, stattdessen Datum+Dauer
als Tooltip beim Hovern/Antippen). Fehlt beim "Zeitraum"-Filter eine
explizite Eingrenzung ("Ganzes Jahr", oder "Frei wählbar" mit nur einer
gesetzten Seite), wird automatisch das ganze geladene Jahr angezeigt — beim
gerade laufenden Jahr nur bis heute, nicht bis Jahresende, damit keine
leeren Balken für die Zukunft erscheinen. Die Balken teilen sich per CSS
(flex:1 1 0) immer die volle verfügbare Breite, egal wie viele es sind
(bis zu 366 bei "Ganzes Jahr") — kein horizontales Scrollen nötig, passt
sich auch bei einer Fenstergrösse-Änderung von selbst neu an.
Belege (Foto oder PDF) erfassen, nach Nextcloud hochladen und als Buchung für Banana vorbereiten. Die kurze Bedienungsanleitung steht nicht mehr fest auf der Seite, sondern hinter dem ?-Symbol ("Hilfe") neben Startseite/ Einstellungen oben rechts.
V* bei Einnahme,
Vorsteuer-Codes M*/I* bei Ausgabe), ebenso die Kategorie-Auswahl (nur
Erlöse bei Einnahme, nur Aufwände bei Ausgabe).RECEIPT_TARGET_FOLDER_PATH (Standard
Buero/Admin/Finanzen in quittung/app.js) plus automatisch das
aktuelle Jahr als letzte Ebene, z.B. Buero/Admin/Finanzen/2026.[JJ]-[A|E][NNN], z.B. 26-A003 — JJ =
aktuelles Jahr (2-stellig), A/E = Ausgabe/Einnahme, NNN =
dreistellig fortlaufend, getrennt gezählt pro Jahr und Typ. Die App
liest den Zielordner per WebDAV (PROPFIND) und zählt weiter.<canvas> gezeichnet, als JPEG re-encodiert und roh
(DCTDecode) in ein von Hand zusammengesetztes Ein-Seiten-PDF
eingebettet (A4, Bild zentriert/eingepasst, Hoch- oder Querformat je
nach Seitenverhältnis) — siehe imageFileToPdfBlob()/
buildSingleImagePdf() in quittung/app.js.[Belegnummer] [Verwendungszweck, max. 60 Zeichen].pdf in diesen Ordner hochgeladen, z.B.
26-A003 KUARIO Quittung.pdf.buchungen.txt im selben
(Jahres-)Ordner angehängt.Wichtig: Diese Funktion braucht zwingend eine Internetverbindung (die nächste Belegnummer wird live aus dem Ordnerinhalt ermittelt) — kein Offline-Modus.
Unter dem Button zeigen zwei Listen ("Letzte Ausgaben"/"Letzte Einnahmen",
je Nummer/Bezeichnung) die letzten 5 bzw. 2 Belege — anhand der
tatsächlich im Zielordner liegenden Belegdateien ermittelt (Dateiname
[Belegnummer] [Bezeichnung].ext), nicht aus buchungen.txt (die kann
z.B. nach einem Banana-Import geleert werden, während die Beleg-PDFs
bleiben), neueste zuerst. Bewusst ohne Betragsspalte: der Betrag steht
nur in buchungen.txt und wäre gerade für ältere Belege oft nicht (mehr)
auffindbar — Nummer und Bezeichnung reichen als schneller Überblick.
Aktualisiert sich nach jedem gespeicherten Beleg sowie beim Zurückkehren in
die App.
Banana Buchhaltung importiert kein CSV, sondern nur sein eigenes generisches Tab-getrenntes TXT-Format "Bewegungen Einnahmen-Ausgaben". Die offizielle Doku nennt die Spalten "DocInvoice"/"ContraAccount" — im tatsächlichen Import-Dialog von Banana heissen sie aber "Doc"/"Category" (im Test bestätigt, die Doku ist an der Stelle ungenau):
Date Description Income Expenses Doc Category Account VatCode
2026-01-15 KUARIO Quittung 45.90 26-A003 4000 6500 M81
(Date im Format yyyy-mm-dd, Beträge mit Punkt als Dezimaltrennzeichen,
kein Tausendertrennzeichen.) Die Spalten werden so befüllt:
| Datei-Spalte | Quelle |
|---|---|
Date | Datum aus dem Formular |
Description | Verwendungszweck (voller Text, nicht gekürzt) |
Income / Expenses | Betrag, je nachdem ob Einnahme oder Ausgabe |
Doc | Belegnummer, z.B. 26-A003 |
Category | Kategorie/Gegenkonto |
Account | Konto |
VatCode | MwSt/USt-Code |
buchungen.txt ist bewusst nicht direkt die Banana-Buchhaltungsdatei
(deren .ac2/natives Format ist proprietär), sondern eine Warteschlange zum
Import: in Banana über Datei → Import → Bewegungen importieren einlesen.
Kontenplan, Kategorien und MwSt/USt-Codes werden nicht in quittung/app.js
hart hinterlegt, sondern zur Laufzeit aus drei Textdateien geladen —
unterschiedlich abgelegt, je nachdem ob sie Projekt-/Kundennamen
enthalten:
mwst.txt: reine, nicht auftragsbezogene Banana-Referenzliste der
MwSt/USt-Codes — liegt im quittung/-Ordner im Repo, vom gleichen Origin
wie die App selbst ausgeliefert (GitHub Pages), reicht ein einfacher
fetch() (kein Nextcloud-Proxy nötig, kein CORS-Thema).konten.txt/kategorien.txt: die Kategorien enthalten Kontonummern
mit Projekt-/Kundenbezug (z.B. Adressen laufender Aufträge als
Kategorie-Bezeichnung) — liegen deshalb vertraulich auf Nextcloud unter
Buero/Admin/App/Finanzen/ statt im (öffentlichen!) Repo, per
Nextcloud-Proxy geladen (braucht deshalb ein Login, siehe unten). Lokal
bei dir bleiben sie trotzdem als Arbeitskopie in quittung/konten.txt/
kategorien.txt bestehen (in .gitignore eingetragen, damit sie nicht
versehentlich wieder committet werden).Format (beide Fälle gleich): Tab-getrennte Zeilen Code<TAB>Bezeichnung,
ein Eintrag pro Zeile — exakt der Export aus Banana (Datei → Export →
Daten für Excel/Open Office/…, Tabellen "Accounts"/"Categories"/
"VatCodes", dort die Beträge-Spalten weglassen/löschen). Zeilen ohne Code
(Leerzeilen, Abschnittsüberschriften, Total-Zeilen) werden beim Einlesen
automatisch übersprungen; bei kategorien.txt werden die Überschriften
"ERLÖSE" und "AUFWÄNDE" als Gruppen erkannt.
Ändert sich der Kontenplan in Banana:
mwst.txt: Datei in Banana neu exportieren, Beträge-Spalten entfernen,
quittung/mwst.txt ersetzen, committen und pushen — die App übernimmt die
Änderung automatisch (alle 60s sowie beim Öffnen des Beleg-Formulars),
ohne Code-Update.konten.txt/kategorien.txt: Datei in Banana neu exportieren,
Beträge-Spalten entfernen, die lokale quittung/konten.txt/
kategorien.txt ersetzen (nur als eigene Arbeitskopie/Referenz) und die
aktualisierte Datei direkt in Nextcloud unter
Buero/Admin/App/Finanzen/ hochladen (ersetzen) — kein
Commit/Push nötig, die App lädt beim nächsten Öffnen/alle 60s die
Nextcloud-Version neu.MWST_EINNAHME_CODES/MWST_AUSGABE_CODES in quittung/app.js legen fest,
welche Codes aus mwst.txt überhaupt zur Auswahl stehen (aktuell nur die
gültigen Sätze 0/2.6/3.8/8.1%) — das ändert sich praktisch nie und bleibt
deshalb hart hinterlegt; nur die Beschreibungstexte kommen live aus
mwst.txt.
Für den allerersten Start ohne Internet/Login gibt es zusätzlich
FALLBACK_KONTEN/FALLBACK_KATEGORIEN/FALLBACK_MWST_CODES in
quittung/app.js als Fallback (danach übernimmt der localStorage-Cache der
zuletzt erfolgreich geladenen Dateien diese Rolle) — bewusst ohne die
auftragsspezifischen Kategorien-Zeilen, da dieser Fallback im (öffentlichen)
Quellcode steht.
Sicherheitshinweis: Ein roher Banana-Export (mit Beträgen) enthält reale
Umsatz-/Aufwandszahlen pro Konto — so eine Datei niemals ins Repo
committen, da dieses Repo öffentlich auf GitHub liegt (GitHub Pages braucht
ein öffentliches Repo). *.jcsv-Dateien sind deshalb in .gitignore
eingetragen — vor jedem Export sicherstellen, dass in
konten.txt/kategorien.txt/mwst.txt wirklich nur Code und Bezeichnung
stehen, keine Beträge-Spalten.
Mehrere strukturierte JSON-Zusammenfassungen von Architekturwettbewerbs-Ausschreibungen hochladen, auf Nextcloud sichern (geräteübergreifend verfügbar) und tabellarisch vergleichen.
.json-Dateien auswählen. Jede muss
mindestens ein Feld projektname haben, sonst wird sie mit Fehlermeldung
übersprungen. Erwartetes Schema: Felder wie auftraggeber, bausumme,
groesse, termine, preisgeld, sachjury, fachjury,
verfahrenssekretariat, …WETTBEWERB_TARGET_FOLDER_PATH (Standard
Buero/Admin/App/Wettbewerbe in wettbewerbsprogramme/app.js), kein
Jahresordner (Wettbewerbe sind nicht zwingend jahresgebunden). Jede Datei
wird unter ihrem (bereinigten) Originaldateinamen abgelegt; erneutes
Hochladen derselben Datei überschreibt die bestehende Version.localStorage-Cache des zuletzt geladenen Datensatzes als
Offline-Fallback; Refresh automatisch alle 60s sowie beim Zurückkehren in
den Tab, oder manuell über "Aktualisieren".Diese Funktion braucht (wie Quittung) DELETE als erlaubte HTTP-Methode im
Cloudflare Worker — siehe Abschnitt "Worker deployen" oben.
Offerten und Rechnungen (gleiches Formular/Datenmodell, unterschieden durch das Feld "Typ") aus einzelnen Positionen (Phasen-Überschriften und Module) zusammenstellen, beliebig hoch-/runterschieben, Zwischentotal / MWST / Total automatisch berechnen, auf Nextcloud sichern und als PDF exportieren. Offerten/Rechnungen lassen sich duplizieren, um für ähnliche Aufträge nicht alles neu erfassen zu müssen.
STATUS_LABELS/
statusKeysForTyp() in offerten/app.js) — Offerten dagegen bewusst nur
zwei (In Bearbeitung / Versendet, kein "Bezahlt": man bezahlt kein
Angebot). Da "Versendet" bei einer Offerte damit bereits der Endzustand
ist, erscheint es dort grün (wie "Bezahlt" bei Rechnungen), während es bei
Rechnungen weiterhin neutral-blau bleibt (dort ist ja erst "Bezahlt" der
Abschluss). Beim Umschalten des Typs im Editor bleibt eine gültige
Status-Auswahl erhalten, eine bei Rechnung gesetzte "Bezahlt"-Auswahl
fällt beim Wechsel zu Offerte auf "In Bearbeitung" zurück.absender.json (siehe unten), kein
eigenes Feld pro Offerte/Rechnung. Darunter die Unterschrift(en)
(siehe eigener Abschnitt unten).Diese Funktion braucht (wie Quittung und Wettbewerbsprogramme) DELETE als
erlaubte HTTP-Methode im Cloudflare Worker — siehe Abschnitt
"Worker deployen" oben.
Enthält echte Geschäftsdaten (Name, Adresse, PLZ/Ort, Ort fürs "Ort, Datum"
in der Brief-Datumszeile, Telefon, E-Mail, Website) — liegt deshalb
vertraulich auf Nextcloud statt im (öffentlichen!) Repo, analog zu
konten.txt/kategorien.txt bei Quittung. Bewusst nicht pro
Offerte/Rechnung erfasst, da praktisch immer gleich.
Buero/Admin/App/Offerten und Rechnungen/_absender.json anlegen (im
selben Ordner wie die Offerten-Dateien selbst, aber kein eigener
Offerten-Datensatz — wird beim Auflisten der Offerten ausdrücklich
ausgeschlossen, siehe ABSENDER_FILENAME):
{
"name": "Muster Architekten AG",
"adresse": "Musterstrasse 12",
"plz_ort": "8000 Zürich",
"ort": "Zürich",
"telefon": "+41 44 000 00 00",
"email": "[email protected]",
"website": "www.muster-architekten.ch"
}
Die App lädt die Datei zur Laufzeit per Login (proxyFetch, siehe
loadAbsender() in offerten/app.js) und hält den zuletzt geladenen Stand
zusätzlich in localStorage als Offline-Fallback. Fehlt die Datei (noch)
auf Nextcloud, bleibt der Briefkopf im PDF einfach leer, statt dass der
Export fehlschlägt. Wird für den PDF-Briefkopf verwendet (absender in
offerten/app.js).
Wählbare Unterzeichner (Checkboxen im Editor, nach dem Brieftext, Mehrfach-
auswahl möglich) kommen aus mitarbeitende in der zentralen
Buero/Admin/App/config.json (siehe Abschnitt "Zentrale Konfiguration" oben)
— dieselbe Liste, die auch bei den Pendenzen fürs Zuordnen/Filtern verwendet
wird (eine Person einmal pflegen statt pro Modul zu duplizieren).
datei (die Unterschrift-PNG) wird nur von den Offerten verwendet, alle
anderen Module ignorieren dieses Feld einfach.
Beim PDF-Export werden die PNGs der ausgewählten Personen live von Nextcloud
geladen (Ordner SIGNATURE_FOLDER_PATH in offerten/pdf.js, Standard
Buero/Admin/KLG und Rechtliches/Unterschriften, im Nextcloud-Konto der
gerade angemeldeten Person) und auf Seite 1 unter dem Brieftext eingefügt
— Bild und Name nebeneinander, an der Unterkante ausgerichtet. Damit eine
Person auch die Unterschrift der anderen einfügen kann, muss die
entsprechende Datei unter demselben Pfad in beide Nextcloud-Konten
gelegt werden (jedes Konto ist ja komplett getrennt, siehe oben) — das ist
manuell zu pflegen, es gibt keinen automatischen Abgleich. Schlägt der
Abruf einer einzelnen Datei fehl (falscher Dateiname, Datei fehlt), wird
das nur als Warnung nach dem Erstellen angezeigt; das PDF entsteht trotzdem,
einfach ohne diese Unterschrift.
Sicherheitsüberlegung: Der Abruf läuft über denselben authentifizierten Kanal (App-Passwort) wie alle anderen Dokumente dieser App, ist also nicht unsicherer als der Rest. Das eigentliche Risiko liegt woanders: Ein Bild einer Unterschrift lässt sich, sobald es in einem verschickten PDF steckt, von praktisch jedem Empfänger wieder herauskopieren und auf andere Dokumente einfügen — das gilt für jede Stempel-/Bild-Unterschrift, unabhängig von Nextcloud oder dieser App. Für Offerten/Rechnungen ist das gängige Praxis, ersetzt aber keine rechtsverbindliche Unterschrift für Dokumente mit höheren Formanforderungen (z.B. Verträge).
"PDF erstellen" (im Editor, oder 📄 pro Zeile in der Liste) erzeugt ein zweiseitiges (bzw. mehrseitiges, je nach Länge) PDF und lädt es direkt im Browser herunter:
absender.json), Betreff,
Brieftext (mit Zeilenumbruch = Absatz, automatischem Zeilenumbruch bei
langen Zeilen), danach optional die gewählten Unterschriften (Bild + Name
nebeneinander, siehe Abschnitt "Unterschriften" oben).fonts/Nudica-Light.otf /
Nudica-Medium.otf / Nudica-LightItalic.otf für Bemerkungen, nicht
die woff/woff2 fürs Web-UI), eingebettet ohne Subsetting — mit Subsetting
erzeugt die verwendete Bibliothek (pdf-lib + fontkit) mit diesen
Schriften eine von manchen PDF-Readern abgelehnte Einbettung.offerten/pdf.js (nutzt nichts aus app.js selbst — die
Unterzeichner-Konfiguration wird als Parameter übergeben statt dort
geladen — aber wie app.js proxyFetch/davPath/ncSegments/authHeader aus
../shared/common.js fürs Laden der Unterschriften-PNGs), gebaut mit
pdf-lib + @pdf-lib/fontkit (siehe
<script>-Tags in index.html, Version dort gepinnt). Läuft komplett im
Browser, kein Server/Backend nötig.Firmen- und Personenadressen filtern, in benannten Ansichten speichern und auf Nextcloud sichern. Nur für den Desktop-Browser gedacht (keine mobile Breitenbeschränkung wie bei den anderen Apps) — die Tabelle ist so breit wie das Browserfenster.
Buero/Admin/App/Adressen (wie bei den Offerten: ein File pro
Datensatz statt einer grossen Liste). Diese Funktion braucht deshalb PUT,
GET, PROPFIND, MKCOL und DELETE — siehe ALLOWED_METHODS in worker.js.adressliste/plz-data.js, Koordinaten pro PLZ-Zentrum von
GeoNames, CC BY 4.0); die Liste sortiert sich gleich danach (nicht
auflösbare Einträge bleiben dabei immer am Ende, unabhängig von der
Sortierrichtung). Die Spalte "Distanz" hat zusätzlich ihr eigenes
Eingabefeld für dieselbe Referenz-PLZ — beide schreiben denselben Wert
(currentView.filters.distanz, landet darum automatisch mit in
gespeicherten Ansichten), blenden aber bewusst keine Zeilen aus: die
Liste wird sortiert, nicht eingeschränkt. Einmal gesetzt, bleibt die
Referenz-PLZ bestehen, auch wenn im "Ort"-Filter danach wieder normal
nach einem Ortsnamen gesucht wird (z.B. "nur Kontakte mit 'Muster AG' im
Ort, sortiert nach Distanz ab 8000") — zum Ändern/Löschen ist danach die
Distanz-Spalte selbst die massgebliche Stelle. Das Feld "Ort" wird als
"PLZ Ortsname" gelesen; fehlt die PLZ (ältere Einträge haben teils nur
den Ortsnamen), wird der Ortsname gegen dieselbe Liste nachgeschlagen —
bei mehrdeutigen Ortsnamen (z.B. gleichnamige Orte in verschiedenen
Kantonen) nicht zwingend der richtige Treffer, ein bewusst in Kauf
genommenes Limit dieser Heuristik. Nicht auflösbare Einträge (leeres oder
unbekanntes "Ort"-Feld) zeigen "?" statt einer Distanz._optionen.json, im selben Nextcloud-Ordner) statt aus freiem Text —
Dropdowns statt Textfelder, sowohl im Editor als auch direkt in der
Tabelle (zusammen mit Kontaktperson und Weihnachtskarte lassen sich diese
vier Felder ändern, ohne den Editor zu öffnen). Neue Werte lassen sich
über "+ neu…" in jedem Dropdown oder über den Button "⚙ Optionen"
hinzufügen; Entfernen über "⚙ Optionen" löscht nur aus der Auswahlliste,
nicht aus bereits gespeicherten Kontakten mit diesem Wert. Beim
allerersten Start werden die Listen automatisch aus den schon
vorhandenen Kontakten befüllt. liegt im selben Ordner wie
die Kontakt-Dateien und wird beim Laden der Kontaktliste ausdrücklich
ausgeschlossen (-Filter in ) — sonst
würde sie selbst als leerer Phantom-Kontakt mit allen Status-Werten
angezeigt.Einfache To-do-Liste, nach Projekt und Person filterbar.
Buero/Admin/App/Pendenzen/pendenzen.json — bei kurzen, oft schnell
angehakten/ergänzten Texten wäre eine Datei pro Pendenz nur Overhead.updatedAt gewinnt, nur
lokal oder nur serverseitig bekannte Pendenzen bleiben in jedem Fall
erhalten. Anders als bei den Offerten (Feld-Merge) ist das ein Merge auf
Ebene ganzer Listeneinträge, ohne Nachfrage-Dialog. Endgültiges Löschen
(siehe unten) markiert deshalb nur geloescht:true statt den Eintrag
sofort aus der Liste zu entfernen — ein Gerät mit veraltetem lokalem Stand
(z.B. ein lange nicht geöffneter Tab) kennt eine echte Entfernung sonst
nicht und würde die längst gelöschte Pendenz beim nächsten Speichern über
die "nur lokal bekannt bleibt erhalten"-Regel wieder zurückschreiben —
genau das sorgte vor der Umstellung auf Tombstones gelegentlich dafür,
dass alte erledigte Pendenzen plötzlich wieder auftauchten. Als
geloescht markierte Pendenzen werden überall ausgeblendet und nach 30
Tagen beim nächsten Sync endgültig aus der Datei entfernt, damit sie nicht
für immer Spuren hinterlassen.projekte.txt wie die
Zeiterfassung, damit ein Projekt überall gleich heisst und gleich aussieht
— Zuordnung/Filter/Farbe erfolgen dabei über den Projektnamen (siehe
Abschnitt "Zentrale Konfiguration" oben), eine Pendenz speichert im Feld
projekt also den Namen,
nicht eine Nummer. Ein Klick auf einen Projekt-Button
filtert die Liste; eine neue Pendenz wird automatisch dem gerade
ausgewählten Projekt zugeordnet ("Alle" → Pendenz ohne Projekt). Die ganze
Zeile nimmt die Projektfarbe als Hintergrund an (nicht nur ein schmaler
Rand) — der Projektname steht bewusst nicht zusätzlich als Text daneben,
die Farbe reicht zum Erkennen, Platzgründe. Text/Icons wechseln je nach
Helligkeit der Farbe automatisch zwischen Weiss und der normalen
Textfarbe (isDarkColor() in pendenzen/app.js, grobe Luma-Schwelle,
keine volle WCAG-Kontrastprüfung).mitarbeitende in derselben config.json (gemeinsam mit den
Offerten, siehe Abschnitt "Unterschriften" dort — eine Person einmal
pflegen statt pro Modul zu duplizieren). Ein eigenes Personen-Dropdown beim
Erfassen gibt es
bewusst nicht (mehr): wie beim Projekt kommt die Person rein aus dem
aktiven Personen-Filter, das deckt den Bedarf schon ab. In der Zeile
selbst erscheint nur das Kürzel (Initialen), der volle Name als Tooltip
bzw. im Bearbeiten-Modus.Sitzungsprotokolle (Aktennotizen) erfassen: Header (Sitzungstitel, Projekt, Datum/Zeit, Ort), Teilnehmende mit optionalem Kürzel, Hauptteil als einfache Liste aus Zwischentiteln und Stichpunkten (Struktur analog zu Phasen/Modulen bei den Offerten), PDF-Export, auf Nextcloud gesichert (ein File pro Protokoll, wie bei den Offerten).
Buero/Admin/App/Protokolle, ein JSON pro Protokoll.roundedTimeNow() in
protokoll/app.js) — beides lässt sich wie gewohnt anpassen.projekte.txt wie
Zeiterfassung/Pendenzen (siehe Abschnitt "Zentrale Konfiguration" oben).
Anders als bei Zeiterfassung/Pendenzen (nur der Name) zeigen Auswahl,
Liste und PDF hier Projektnummer UND Name ("021 – Neubau Werkhof").mitarbeitende in derselben
config.json) lassen sich per "+ Name"-Knopf hinzufügen und bekommen ihr Kürzel
automatisch (Initialen, nicht änderbar); externe Teilnehmende werden über
"+ Teilnehmer/in" als freie Zeile angelegt.Buero/Admin/App/Pendenzen/pendenzen.json angelegt/aktualisiert (Person und
Text aus dem Protokoll; als Projekt wird der reine Name des im Protokoll
gewählten Projekts übernommen, nicht die Nummer — Pendenzen ordnet Projekte
namensbasiert zu, siehe Abschnitt "Zentrale Konfiguration" oben). Die
Pendenz-ID ist über Protokoll- und Stichpunkt-ID stabil: ein erneutes
Speichern aktualisiert denselben Eintrag (Textänderungen werden
übernommen), statt zu duplizieren, und ein in der Pendenzen-App bereits
gesetztes Häkchen bzw. eine manuell geänderte Reihenfolge bleiben dabei
erhalten. Wird das Kürzel wieder entfernt oder der Stichpunkt gelöscht,
verschwindet die zugehörige, vom Protokoll selbst angelegte Pendenz auch
wieder. Kein Merge mit gleichzeitigen Änderungen aus der Pendenzen-App
selbst (anders als deren eigener Sync) — Protokolle werden dafür deutlich
seltener gespeichert als einzelne Pendenzen bearbeitet.Terminplanung als horizontaler Zeitbalken (Gantt-artig), rein browserbasiert
(kein PDF-Export). Der Ordner heisst timeline/ (das Modul hiess früher
"Zeitplanung"/zeitplanung/). Unter der alten Adresse .../zeitplanung/
liegt bewusst ein minimaler Redirect-Stub (zeitplanung/index.html +
eigener service-worker.js), damit bestehende Lesezeichen/Home-Bildschirm-
Icons nicht ins Leere laufen, sondern automatisch nach ../timeline/
weitergeleitet werden — inkl. eigener Service-Worker-Aktualisierung, damit
das auch für bereits offene alte Tabs zuverlässig funktioniert (siehe
Abschnitt "Bekannte Grenzen"/registerServiceWorkerWithAutoUpdate() in
shared/common.js). Die ausführliche Bedienungsanleitung steht nicht mehr
fest auf der Seite, sondern hinter dem ?-Symbol ("Hilfe") neben Startseite/
Einstellungen oben rechts. Das Zeitfenster ist immer "heute bis in 2 Jahre" —
rollend, kein festes Kalenderjahr; beim erneuten Öffnen verschiebt es sich
also automatisch mit.
Buero/Admin/App/Timeline/zeitplanung.json (wie bei den Pendenzen) statt
einer Datei pro Projekt/Aufgabe.Mitarbeitende (aus mitarbeitende in der zentralen config.json,
siehe Abschnitt "Zentrale Konfiguration" oben) — hier Ferien/Frei
eintragen (Balken immer grau). Frei/Ferien-Balken entstehen bewusst nur
durch Ziehen (immer mehrtägig, kein Meilenstein per Klick wie bei
Aufgaben). Jeder Tag, an dem mindestens eine Person einen Balken hat,
dessen Titel "ferien", "frei", "weg" oder "abwesend"
enthält (als Teilstring, nicht nur exakt — erkennt also auch z.B.
"Weihnachtsferien"), bekommt einen hellgrauen Streifen über alle
Zeilen hinweg — je mehr Personen an diesem Tag frei haben, desto dunkler
der Streifen (isFreiTitle()/renderVacationOverlay() in
timeline/app.js).
Über das 🗓-Symbol neben dem Namen lässt sich pro Person ein Wochenschema hinterlegen (an welchen Wochentagen sie regulär da ist, wahlweise "jede Woche" oder "jede 2. Woche" ab einem gewählten Ankerdatum) — die zutreffenden Tage werden in der jeweiligen Zeile grünlich markiert, gelten automatisch fürs ganze (rollende) Jahr und blenden sich an Tagen mit einem Ferien/Frei-Balken von selbst aus. Samstage/Sonntage ohne Anwesenheit sind stattdessen grau hinterlegt (ein Tag ist also immer entweder grünlich oder grau, nie beides). Ein einfacher Klick auf einen Tag in der Mitarbeiter-Zeile schaltet dessen Anwesenheit einzeln um: ein grünlich markierter Tag wird zur Ausnahme (z.B. ein einzelner freier Montag ohne eigenen Ferien-Eintrag), ein nicht markierter Tag (z.B. ein Sonntag) wird zum einzelnen Zusatztag (ein ausserordentlicher Einsatz ausserhalb des Schemas) — nochmaliges Anklicken macht das jeweils rückgängig.
Projekte — entweder per Dropdown aus derselben zentralen projekte.txt
wie Zeiterfassung/Pendenzen (siehe Abschnitt "Zentrale Konfiguration"
oben; Zuordnung/Farbe über den Namen, nicht die Nummer) oder frei benannt
über das Textfeld daneben — für Vorhaben, die
(noch) nicht in der offiziellen Liste stehen. Auf Klick auf den
Projekttitel lässt er sich frei umbenennen (unabhängig davon, ob der neue
Name in der zentralen Liste vorkommt); die Pfeile ▲▼ (erscheinen beim
Überfahren der Zeile) verschieben ein Projekt in der Liste nach oben/
unten. Auf-/zuklappbar; die Projektzeile selbst zeigt dabei immer (auch zugeklappt) alle Balken/
Meilensteine ihrer Aufgaben zusammen als halbtransparente Übersicht,
bewusst ohne Titeltext (reine "Strichcode"-Ansicht) — laufen mehrere
Aufgaben gleichzeitig, zeichnet sich das durch die Überlagerung als
dunklere Fläche ab.
Liquiditätsplanung pro Kalenderjahr (fest, nicht rollend wie die Timeline) — Anfangssaldo, monatlich wiederkehrende und einmalige Ausgaben, Einnahmen, Auszahlungen (Gewinnausschüttungen an die Gesellschafter), daraus laufend berechnet der Saldo pro Monat.
Buero/Admin/App/Finanzplanung/<Jahr>.json) — ein Jahr ist in sich
geschlossen, beim Jahreswechsel wird ohnehin komplett neu geladen, keine
Merge-Logik über Jahre hinweg nötig (anders als bei den Pendenzen).‹ Jahr › oben, freie Navigation in beide
Richtungen ohne Begrenzung.SECTIONS in
finanzplanung/app.js).
prompt()-Fenster danach
(leer lassen = keine Info). Erscheint als Tooltip beim Überfahren der
Zahl, zusätzlich per gepunkteter Unterstreichung auch ohne Hover
erkennbar.berechneSalden()), rötlich eingefärbt bei negativem Saldo.icons/home-*.png, icons/zeiterfassung-*.png,
icons/quittung-*.png, icons/wettbewerb-*.png, icons/offerten-*.png,
icons/adressliste-*.png, icons/pendenzen-*.png, icons/protokoll-*.png,
icons/timeline-*.png, icons/finanzplanung-*.png (je 192px + 512px PNG).
Zum Ändern einfach unter denselben Dateinamen ersetzen — keine
Code-/Manifest-Änderung nötig.python3 -m http.server 8080
# im Browser: http://localhost:8080 (Startseite)
# bzw. http://localhost:8080/zeiterfassung/, /quittung/, /wettbewerbsprogramme/,
# /offerten/, /adressliste/, /pendenzen/, /protokoll/, /timeline/
git init
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin https://github.com/<dein-user>/zeiterfassung.git
git push -u origin main
Danach im Repository unter Settings → Pages:
main, Ordner / (root)Nach 1–2 Minuten erreichbar unter https://<dein-user>.github.io/zeiterfassung/.
Als eigene Apps installieren: Auf dem Handy die gewünschte Unterseite
öffnen (.../zeiterfassung/, .../quittung/ oder
.../wettbewerbsprogramme/) und dort "Zum Home-Bildschirm hinzufügen" —
jede landet als eigenes Icon mit eigenem Namen. Die Startseite
(.../ ohne Unterordner) lässt sich zusätzlich/alternativ als
Kachel-Menü installieren.
Falls vorher schon die alte (kombinierte) Version installiert war: Das bestehende Homescreen-Icon zeigt nach diesem Update auf die neue Startseite statt direkt auf die Zeiterfassung, da Root jetzt der Launcher ist — altes Icon löschen und die gewünschten Apps wie oben neu hinzufügen.
1)/2)/… vor den Modultiteln aus — im Web-Formular wie im PDF.1), 2), …
nach Position unter den Modulen, nicht Teil der Daten, abschaltbar siehe
oben) mit Kurzbeschrieb darunter — ein Punkt pro Zeile im Textfeld,
gespeichert als Liste für eine Bulletpoint-Darstellung im PDF —, Stunden
(Zahlenfeld ohne Zähler-Pfeile, Einheit "Std." direkt daneben) und daraus
berechnete Kosten = Stunden × Stundensatz. Über "+ Modul hinzufügen"
ergänzen. Die Spaltenbeschriftung ("Modul"/"Stunden"/"Kosten") ist im
Web-Formular bewusst weggelassen (selbsterklärend); im PDF steht sie
weiterhin da.bemerkungAktiv);
der Text selbst (bemerkung) bleibt beim Abwählen erhalten, falls man
ihn später wieder einblenden will.allKnownModules()/searchModules() in offerten/app.js.renderPositionen() in offerten/app.js.chFrRounded()/chFrRoundedPdf().localStorage, offerten_stundensatz), wird nur als Vorschlag für
neue Offerten verwendet. Der tatsächlich verwendete Satz wird pro
Offerte mitgespeichert — ändert sich später die Vorgabe, bleiben bereits
gespeicherte Offerten unverändert (keine rückwirkende Neuberechnung).OFFERTEN_TARGET_FOLDER_PATH (Standard
Buero/Admin/App/Offerten und Rechnungen in offerten/app.js), kein
Jahresordner. Neue Offerten werden als [Datum] [Projekt].json abgelegt;
beim Bearbeiten einer bestehenden Offerte bleibt der Dateiname unverändert
(Überschreiben statt Duplikat), auch wenn sich Datum/Projekt ändern.offerten/service-worker.js_optionen.jsonOPTIONS_FILENAMErefreshContacts()config.json (siehe
Abschnitt "Zentrale Konfiguration" oben) — kein "+ neu…", keine Verwaltung
über "⚙ Optionen". Ein gespeicherter Wert, der zu niemandem in der
aktuellen Mitarbeitenden-Liste mehr passt (z.B. nach einem Namenswechsel),
bleibt trotzdem als Option erhalten statt zu verschwinden.Buero/Admin/App/Adressen/Ansichten,
also für alle Personen mit Zugriff auf diesen Ordner gleich sichtbar. Ist
gerade eine gespeicherte Ansicht aktiv und man klickt "Ansicht speichern"
mit demselben Namen, wird diese Ansicht überschrieben; gibt man dagegen
einen anderen, noch nicht vergebenen Namen ein, entsteht eine neue
Ansicht (die ursprüngliche bleibt unverändert erhalten) — nur bei einem
Namen, der bereits einer anderen Ansicht gehört, fragt die App vor dem
Überschreiben nach.updatedAt/updatedBy
(Zeitstempel + Anzeigename der zuletzt speichernden Person, sichtbar im
Editor und optional als Spalte "Zuletzt geändert"). Vor dem Speichern eines
bestehenden Kontakts wird der Stand auf Nextcloud nochmals frisch geladen
und mit dem Stand beim Öffnen verglichen — weicht er ab, warnt die App
(wer hat wann geändert) und lässt die Wahl zwischen Überschreiben und
neu laden. Das ist kein echtes Locking (dafür bräuchte es einen Server),
verhindert aber, dass eine fremde Änderung stillschweigend verloren geht.adressliste/etiketten.js, nutzt pdf-lib wie die
PDF-Exporte bei Offerten/Protokoll) — pro Etikett Firma, Vorname/Name,
Strasse, Ort, jeweils nur falls vorhanden; Kontakte ganz ohne eine dieser
Angaben werden übersprungen (Hinweis danach, wie viele). A4-Bogen mit 3
Spalten × 7 Zeilen (21 Etiketten, je ca. 6.3×3.7cm) — ein generisches,
selbst berechnetes Raster statt eines bestimmten Etiketten-Produkts; bei
bereits vorgeschnittenen Bögen vorher einen Probedruck gegenprüfen.<form> mit
submit-Event deckt Klick auf "+" und Enter am Desktop ab, zusätzlich ein
eigener keydown-Handler ausschliesslich fürs Textfeld: in als
Home-Bildschirm-App installierten PWAs auf iOS löst die Eingabetaste/das
Häkchen der virtuellen Tastatur das native Form-Submit bekanntermassen
nicht zuverlässig aus (WebKit-Eigenheit nur im Standalone-Modus, in einem
normalen Safari-Tab funktioniert es). Zusätzlich type="search" statt
"text" fürs Eingabefeld (ein weiterer bekannter, risikoarmer Kniff dafür
-- Suchfelder haben in Safari eine eigene, verlässlichere native
Tastatur-Behandlung). Unverifiziert auf echtem iPhone (im Entwicklungs-
Setup nicht testbar) -- falls es dort immer noch nicht zuverlässig
funktioniert, ist "+" antippen die verlässliche Alternative, dafür bleibt
der Button bewusst bestehen.protokoll/pdf.js,
eigenständig gehalten).Aufgaben — frei benannt (Klick auf "+ Aufgabe"/auf den Titel zum Umbenennen), je Aufgabe eine eigene Zeile. Eine Aufgabe kann beliebig viele Balken (Start–Ende) und/oder Meilensteine (ein einzelnes Datum, als Raute dargestellt) enthalten, jeweils mit eigenem Titel. Jede Raute zeigt den Tag des Monats direkt in der Raute, in der halbtransparenten Projekt-Übersichtszeile ausgeblendet (reine "Strichcode"-Ansicht). Im Bearbeiten-Dialog lässt sich zusätzlich pro Person ein Pensum in % eintragen (ein Meilenstein zählt dabei als eintägiger Balken) — daraus wird automatisch die Gesamtstundenzahl errechnet (Anwesenheitstage der Person im Zeitraum laut Wochenschema × 8h × Pensum, auf ganze Stunden gerundet) und direkt am Balken bzw. neben der Raute angezeigt. Eine Live-Vorschau der Stunden erscheint schon im Dialog, während Daten/Pensum angepasst werden. Zusätzlich steht die Gesamtstundenzahl rechtsbündig neben dem Zeilentitel: pro Aufgabe die Summe all ihrer Balken/Meilensteine, pro Projekt die Summe über alle Aufgaben — bleibt weg, wo nirgends ein Pensum zugewiesen ist.
Das Restpensum (100% minus die Summe aller an diesem Tag über
alle Projekte/Aufgaben hinweg zugewiesenen Pensen) zeigt sich direkt
in der Mitarbeiter-Zeile: ein unverplanter Anwesenheitstag bleibt im
normalen Grün, mit zunehmender Buchung wird der Tag dunkler eingefärbt
und bei Überbuchung (Restpensum <0) rötlich. Die genaue Zahl steht
immer als Tooltip zur Verfügung, ab einer gewissen Zoomstufe auch
direkt sichtbar im Streifen (restpensumPercent()/
attendanceStripesHtml() in timeline/app.js).
DAY_HEADER_MIN_WIDTH in timeline/app.js).visibleBarRect()).prompt()-Fenster
statt eines eigenen Dialogs) — gedacht zum Ausprobieren, danach gezielt
verfeinern.updateOfferStatus() in
offerten/app.js) wird jede Änderung als reine Funktion sowohl sofort
lokal (optimistisch) als auch, seriell in einer Warteschlange, auf einen
frisch vom Server geholten Stand angewendet — kein Vergleichs-/
Konflikt-Dialog nötig, da die Teiländerung gezielt nachgetragen wird statt
den ganzen Stand zu überschreiben. Jahr und Anfangssaldo-Vorschlag werden
dabei beim Auslösen der Änderung eingefroren (nicht erst beim
tatsächlichen Schreiben gelesen) — sonst würde ein noch hängender
Speichervorgang beim schnellen Weiterklicken aufs nächste Jahr versehentlich
dort landen.