From f298802c38c9397b15b3c34238c3fa703a84e85c Mon Sep 17 00:00:00 2001 From: Clemens Creutzburg Date: Sat, 21 Mar 2026 17:32:57 +0100 Subject: [PATCH] feat: Add initial SaaS application structure with .htaccess, index.php, and environment setup scripts - Create .htaccess for Apache front-controller routing - Add README.md for public directory with project overview - Implement index.php as the main entry point with a preview of SaaS modules - Introduce PowerShell scripts to check prerequisites and prepare environment --- README.md | 44 + docs/implementation-foundation.md | 69 +- docs/installationshandbuch.md | 87 ++ docs/legacy-to-saas-mapping.md | 48 + .../DataTables}/datatables.css | 0 .../DataTables}/datatables.js | 0 .../DataTables}/datatables.min.css | 0 .../DataTables}/datatables.min.js | 0 .../PHPMailer}/COMMITMENT | 0 {PHPMailer => legacy-app/PHPMailer}/LICENSE | 0 .../PHPMailer}/composer.json | 0 .../PHPMailer}/get_oauth_token.php | 0 legacy-app/README.md | 25 + {TCPDF => legacy-app/TCPDF}/tcpdf.php | 0 .../TCPDF}/tcpdf_autoconfig.php | 0 .../TCPDF}/tcpdf_barcodes_1d.php | 0 .../TCPDF}/tcpdf_barcodes_2d.php | 0 {TCPDF => legacy-app/TCPDF}/tcpdf_import.php | 0 .../assets}/css/fontawesome-all.min.css | 0 {assets => legacy-app/assets}/css/main.css | 8 +- .../assets}/js/breakpoints.min.js | 2 +- .../assets}/js/browser.min.js | 2 +- .../assets}/js/jquery.min.js | 0 {assets => legacy-app/assets}/js/main.js | 522 ++++---- {assets => legacy-app/assets}/js/util.js | 1172 ++++++++--------- config.php => legacy-app/config.php | 78 +- csvupload.php => legacy-app/csvupload.php | 434 +++--- einzahlung.php => legacy-app/einzahlung.php | 302 ++--- .../exportKaffeeliste.php | 448 +++---- faq.php => legacy-app/faq.php | 178 +-- favicon.ico => legacy-app/favicon.ico | Bin footer.php => legacy-app/footer.php | 122 +- functions.php => legacy-app/functions.php | 136 +- .../functionsLDAP.php | 330 ++--- header.php => legacy-app/header.php | 74 +- headerline.php => legacy-app/headerline.php | Bin hinweise.php => legacy-app/hinweise.php | 188 +-- index.php => legacy-app/index.php | 480 +++---- .../jahresauswertung.php | 354 ++--- {js => legacy-app/js}/dashboard.css | 0 {js => legacy-app/js}/dashboard.js | 0 {js => legacy-app/js}/dashboard.rtl.css | 0 kaffeeliste.php => legacy-app/kaffeeliste.php | 294 ++--- .../letzteneintraege.php | 474 +++---- mailausgebe.php => legacy-app/mailausgebe.php | 98 +- .../mailversenden.php | 282 ++-- .../mitarbeiterverwalten.php | 524 ++++---- .../namenanpassen.php | 242 ++-- nav.php => legacy-app/nav.php | 6 +- .../stricheintragen.php | 320 ++--- .../teilnehmerauswertung.php | 424 +++--- umfrage.php => legacy-app/umfrage.php | 682 +++++----- .../umfrageergebnisse.php | 738 +++++------ watermark.jpg => legacy-app/watermark.jpg | Bin watermark.png => legacy-app/watermark.png | Bin saas-app/README.md | 70 +- saas-app/bootstrap/app.php | 18 +- saas-app/config/multitenancy.php | 6 +- saas-app/public/.htaccess | 6 + saas-app/public/README.md | 9 + saas-app/public/index.php | 234 ++++ .../views/auth/forgot-password.blade.php | 58 +- saas-app/resources/views/auth/login.blade.php | 71 +- .../resources/views/content/index.blade.php | 76 +- .../resources/views/dashboard/index.blade.php | 105 +- .../resources/views/exports/index.blade.php | 73 +- .../resources/views/imports/index.blade.php | 81 +- .../resources/views/layouts/app.blade.php | 373 +++++- .../resources/views/ledger/index.blade.php | 79 +- .../resources/views/members/index.blade.php | 74 +- .../views/notifications/index.blade.php | 82 +- .../resources/views/payments/index.blade.php | 94 +- .../resources/views/surveys/index.blade.php | 40 +- .../resources/views/tenants/index.blade.php | 108 +- saas-app/resources/views/welcome.blade.php | 65 +- scripts/check-prerequisites.ps1 | 48 + scripts/prepare-saas-env.ps1 | 23 + 77 files changed, 6381 insertions(+), 4599 deletions(-) create mode 100644 README.md create mode 100644 docs/installationshandbuch.md create mode 100644 docs/legacy-to-saas-mapping.md rename {DataTables => legacy-app/DataTables}/datatables.css (100%) rename {DataTables => legacy-app/DataTables}/datatables.js (100%) rename {DataTables => legacy-app/DataTables}/datatables.min.css (100%) rename {DataTables => legacy-app/DataTables}/datatables.min.js (100%) rename {PHPMailer => legacy-app/PHPMailer}/COMMITMENT (100%) rename {PHPMailer => legacy-app/PHPMailer}/LICENSE (100%) rename {PHPMailer => legacy-app/PHPMailer}/composer.json (100%) rename {PHPMailer => legacy-app/PHPMailer}/get_oauth_token.php (100%) create mode 100644 legacy-app/README.md rename {TCPDF => legacy-app/TCPDF}/tcpdf.php (100%) rename {TCPDF => legacy-app/TCPDF}/tcpdf_autoconfig.php (100%) rename {TCPDF => legacy-app/TCPDF}/tcpdf_barcodes_1d.php (100%) rename {TCPDF => legacy-app/TCPDF}/tcpdf_barcodes_2d.php (100%) rename {TCPDF => legacy-app/TCPDF}/tcpdf_import.php (100%) rename {assets => legacy-app/assets}/css/fontawesome-all.min.css (100%) rename {assets => legacy-app/assets}/css/main.css (99%) rename {assets => legacy-app/assets}/js/breakpoints.min.js (99%) rename {assets => legacy-app/assets}/js/browser.min.js (99%) rename {assets => legacy-app/assets}/js/jquery.min.js (100%) rename {assets => legacy-app/assets}/js/main.js (95%) rename {assets => legacy-app/assets}/js/util.js (95%) rename config.php => legacy-app/config.php (96%) rename csvupload.php => legacy-app/csvupload.php (96%) rename einzahlung.php => legacy-app/einzahlung.php (96%) rename exportKaffeeliste.php => legacy-app/exportKaffeeliste.php (97%) rename faq.php => legacy-app/faq.php (98%) rename favicon.ico => legacy-app/favicon.ico (100%) rename footer.php => legacy-app/footer.php (95%) rename functions.php => legacy-app/functions.php (96%) rename functionsLDAP.php => legacy-app/functionsLDAP.php (95%) rename header.php => legacy-app/header.php (96%) rename headerline.php => legacy-app/headerline.php (100%) rename hinweise.php => legacy-app/hinweise.php (95%) rename index.php => legacy-app/index.php (97%) rename jahresauswertung.php => legacy-app/jahresauswertung.php (96%) rename {js => legacy-app/js}/dashboard.css (100%) rename {js => legacy-app/js}/dashboard.js (100%) rename {js => legacy-app/js}/dashboard.rtl.css (100%) rename kaffeeliste.php => legacy-app/kaffeeliste.php (96%) rename letzteneintraege.php => legacy-app/letzteneintraege.php (96%) rename mailausgebe.php => legacy-app/mailausgebe.php (94%) rename mailversenden.php => legacy-app/mailversenden.php (96%) rename mitarbeiterverwalten.php => legacy-app/mitarbeiterverwalten.php (96%) rename namenanpassen.php => legacy-app/namenanpassen.php (96%) rename nav.php => legacy-app/nav.php (75%) rename stricheintragen.php => legacy-app/stricheintragen.php (96%) rename teilnehmerauswertung.php => legacy-app/teilnehmerauswertung.php (97%) rename umfrage.php => legacy-app/umfrage.php (97%) rename umfrageergebnisse.php => legacy-app/umfrageergebnisse.php (96%) rename watermark.jpg => legacy-app/watermark.jpg (100%) rename watermark.png => legacy-app/watermark.png (100%) create mode 100644 saas-app/public/.htaccess create mode 100644 saas-app/public/README.md create mode 100644 saas-app/public/index.php create mode 100644 scripts/check-prerequisites.ps1 create mode 100644 scripts/prepare-saas-env.ps1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..b167d33 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# Kaffeeliste SaaS Workspace + +Dieses Repository ist jetzt `SaaS-first` organisiert. + +- `saas-app/` enthaelt die neue Zielanwendung fuer die mandantenfaehige + Kaffeeliste. +- `docs/` enthaelt Architektur-, Installations- und Migrationsdokumentation. +- `legacy-app/` enthaelt den bisherigen PHP-Bestand als archivierte Referenz fuer + Fachlogik und Datenmigration. + +## Produktkern + +Die SaaS-Version behaelt die wesentlichen Funktionen der alten Anwendung bei: + +- persoenliches Dashboard mit Kontostand, Verbrauch und letzten Buchungen +- Mitgliederverwaltung pro Mandant +- Kaffee-Striche, Einzahlungen und Salden als gemeinsames Ledger +- Hinweise, FAQ und tenantbezogene Inhalte +- operative Importe, Exporte und Benachrichtigungen + +Nicht mehr priorisierte Sonderseiten und Einmalskripte bleiben nur noch im +Archivbestand erhalten. + +## Einstieg + +1. Lies [docs/implementation-foundation.md](docs/implementation-foundation.md) + fuer Zielbild und Scope. +2. Nutze [docs/installationshandbuch.md](docs/installationshandbuch.md) fuer + Setup, Hosting und Betriebsablauf. +3. Arbeite anschliessend in [saas-app/README.md](saas-app/README.md) weiter. + +## Hilfsskripte + +- `scripts/check-prerequisites.ps1` prueft lokale Voraussetzungen. +- `scripts/prepare-saas-env.ps1` legt aus `.env.example` eine lokale `.env` an. + +## Hinweise Zum Umbau + +- `legacy-app/` ist absichtlich nicht geloescht, sondern als Referenz fuer die + Daten- und Fachmigration verschoben. +- Das aktuelle `saas-app/` ist eine konsistente Zielarchitektur mit ueberarbeiteten + Views, Modulgrenzen und Betriebsdokumentation. +- Ein vollstaendiges Laravel-Bootstrap mit Composer und Runtime bleibt der + naechste technische Ausbauschritt. diff --git a/docs/implementation-foundation.md b/docs/implementation-foundation.md index c472151..c076f6c 100644 --- a/docs/implementation-foundation.md +++ b/docs/implementation-foundation.md @@ -1,33 +1,52 @@ -# Implementation Foundation +# SaaS First Zielbild -Das neue Zielprojekt liegt in `saas-app/`, damit der bisherige PHP-Bestand -unangetastet als Referenz bestehen bleibt. +Dieses Repository wird schrittweise von der Legacy-PHP-Anwendung auf eine +mandantenfaehige SaaS-Struktur umgestellt. Das neue Zielprojekt liegt in +`saas-app/`. Der Root-Bestand bleibt nur noch als Referenz fuer die bestehende +Fachlogik erhalten. -Foundation-Stand: +## Was in die SaaS-Version gehoert -- Laravel-nahe Ordnerstruktur ohne externe Downloads -- Tenant-Resolution-Skelett ueber Host/Subdomain -- Request-Context-Platzhalter -- Grundrouten fuer Landing, Login und Dashboard -- erste SQL-Migrationsskizzen fuer: - - `tenants` - - `users` - - `tenant_users` - - `roles` - - `tenant_user_roles` -- Blade-Layouts als Platzhalter fuer SSR-Ansatz auf Webspace +Der fachliche Kern der bisherigen Anwendung ist klein und wird in der neuen +Version gebuendelt: -Naechste Programmierphase: +- Dashboard mit Kontostand und aktueller Uebersicht +- Mitgliederverwaltung pro Mandant +- Einzahlungen und Kaffee-Striche als Ledger-Buchungen +- Hinweise, FAQ und einfache Inhalte +- Export- und Import-Funktionen fuer den operativen Betrieb +- Mandanten- und Rollenverwaltung -1. Identity-/Tenant-Agent auf `saas-app/app` und Auth-/Tenant-Module -2. Ledger-/Core-Agent auf Mitglieder, Striche, Einzahlungen und Dashboard -3. spaeter Operations-Agent fuer Importe, Mail, Umfragen und Exporte +## Was bewusst entfallen kann -Blocker ausserhalb des Repos: +Diese Bereiche sind fuer den ersten SaaS-Stand nicht zwingend notwendig und +koennen spaeter nachgezogen oder ganz entfallen: -- `php` lokal nicht installiert -- `composer` lokal nicht installiert -- echtes Laravel-Scaffolding daher noch nicht moeglich +- Jahresauswertung als Sonderprozess +- CSV-Sonderlogiken aus der alten Webanwendung +- manuelle Mail-Sammelaktionen ohne Workflow-Einbindung +- Umfrage-Module, sofern sie fachlich nicht mehr benoetigt werden +- alte PHP-Einzeldateien im Root, sobald die SaaS-Variante produktiv ist -Sobald Tooling vorhanden ist, soll dieses Geruest in ein vollstaendiges -Laravel-Projekt ueberfuehrt werden. +## Migrationspfad + +1. Legacy-Fachlogik aus dem Root als Referenz verstehen. +2. Kernbereiche in `saas-app/` neu umsetzen. +3. Datenmodell auf Mandanten, Benutzer, Mitglieder, Buchungen und Inhalte + normalisieren. +4. Root-Anwendung nur noch fuer die Uebergangsphase vorhalten. +5. Nach erfolgreichem Cutover alte Root-Seiten deaktivieren oder archivieren. + +## Technische Leitplanken + +- `saas-app/` ist die Zielstruktur. +- Der Betrieb ist auf klassisches PHP-Hosting ausgerichtet. +- Cron-Jobs ersetzen dauerhafte Worker. +- OIDC ist der bevorzugte SSO-Pfad. +- Blade-Views und SSR sind fuer einfache Hosting-Setups vorgesehen. + +## Derzeitiger Stand + +Das Zielgeruest ist bewusst schlank gehalten. Es beschreibt die Architektur und +die Aufteilung der Module, ersetzt aber noch kein voll installiertes Laravel- +Projekt mit lauffaehiger Runtime. diff --git a/docs/installationshandbuch.md b/docs/installationshandbuch.md new file mode 100644 index 0000000..93e43ac --- /dev/null +++ b/docs/installationshandbuch.md @@ -0,0 +1,87 @@ +# Installationshandbuch + +Diese Anleitung beschreibt den vorgesehenen Weg fuer die SaaS-Version in +`saas-app/`. Sie ist auf Webspace-Betrieb ohne Docker und ohne dauerhafte +Worker ausgelegt. + +## Voraussetzungen + +- PHP 8.2 oder neuer +- Composer +- SQL Server oder eine kompatible Datenbank fuer das Zielsystem +- Webserver mit Document-Root auf `public/` des Zielprojekts +- Cron-Zugang +- optional: SMTP-Zugang fuer Mails +- optional: OIDC-Provider fuer SSO + +## Installation lokal + +1. Repository klonen. +2. In `saas-app/` wechseln. +3. Abhaengigkeiten mit Composer installieren. +4. `.env` aus `.env.example` ableiten. +5. Datenbankzugang, URL, Mail und Tenancy-Werte eintragen. +6. Migrations ausfuehren. +7. Einen ersten Mandanten anlegen. +8. Einen ersten Benutzer und eine Mitgliedszuordnung anlegen. + +## Wichtige Umgebungswerte + +Die wichtigsten Werte liegen in `saas-app/.env.example` und muessen fuer die +eigene Umgebung angepasst werden: + +- `APP_URL` +- `DB_HOST`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD` +- `TENANCY_MODE` +- `TENANCY_CENTRAL_DOMAINS` +- `TENANCY_FALLBACK_TENANT` +- `OIDC_ENABLED` +- Mail-Zugangsdaten + +## Webspace-Deployment + +1. Den Inhalt von `saas-app/` auf den Zielserver hochladen. +2. Das Document-Root auf `public/` zeigen lassen. +3. Schreibrechte fuer `storage/`, Cache und Queue-Tabellen sicherstellen. +4. `.env` serverseitig hinterlegen. +5. Die Anwendung einmal per Browser aufrufen und die Grundseiten pruefen. + +## Cron- und Batch-Betrieb + +Die Zielarchitektur nutzt Cron statt dauerhafter Worker. + +Typische Aufgaben: + +- Queue-Jobs verarbeiten +- Import-Jobs abarbeiten +- Export-Jobs erzeugen +- Benachrichtigungen versenden +- Aufraeum- und Statusjobs ausfuehren + +Empfehlung: pro Aufgabe einen klar benannten Cron-Eintrag anlegen und die +Ausgabe in Logdateien schreiben. + +## Migration aus dem Legacy-System + +Wenn Daten aus der alten Root-Anwendung uebernommen werden sollen, folgt die +Reihenfolge: + +1. Mitglieder und Rollen migrieren. +2. Einzahlungen und Kaffeebuchungen uebernehmen. +3. Hinweise, FAQ und Inhaltsseiten importieren. +4. Mandanten-Zuordnung pruefen. +5. Danach alte Root-Seiten nur noch lesend oder gar nicht mehr betreiben. + +## Betriebscheck + +Nach dem Setup sollten diese Punkte funktionieren: + +- Login oder SSO-Startseite +- Dashboard pro Mandant +- Mitgliederliste +- Buchungen und Kontostand +- Hinweise und Content +- Import- und Export-Status + +Wenn einer dieser Bereiche fehlt, liegt meist entweder ein Tenancy-Fehler, ein +DB-Problem oder eine unvollstaendige Deployment-Konfiguration vor. diff --git a/docs/legacy-to-saas-mapping.md b/docs/legacy-to-saas-mapping.md new file mode 100644 index 0000000..5d46fd3 --- /dev/null +++ b/docs/legacy-to-saas-mapping.md @@ -0,0 +1,48 @@ +# Legacy Zu SaaS Mapping + +Diese Uebersicht verbindet die bisherige Root-Anwendung mit der neuen +SaaS-Zielstruktur in `saas-app/`. + +## Kernmodule + +| Legacy-Datei | Bisherige Aufgabe | SaaS-Zielmodul | +| --- | --- | --- | +| `legacy-app/index.php` | persoenliches Dashboard, Kontostand, letzte Buchungen, Schnellaktion fuer Striche | Dashboard, Ledger, Payments | +| `legacy-app/stricheintragen.php` | Sammelerfassung von Kaffee-Strichen | Ledger | +| `legacy-app/einzahlung.php` | Sammelerfassung von Einzahlungen | Payments | +| `legacy-app/kaffeeliste.php` | operative Gesamtuebersicht aller Mitglieder | Members, Ledger | +| `legacy-app/mitarbeiterverwalten.php` | Mitgliederpflege, Aktivstatus, Admin-Rolle | Members, Tenants | +| `legacy-app/letzteneintraege.php` | Korrektur und Loeschung letzter Buchungen | Ledger | +| `legacy-app/teilnehmerauswertung.php` | Detailauswertung pro Person | Dashboard, Members, Exports | +| `legacy-app/namenanpassen.php` | Pflege des Anzeigenamens | Members, Identity | +| `legacy-app/hinweise.php` | Pflege globaler Hinweise | Content | +| `legacy-app/faq.php` | statische Hilfeseite | Content | + +## Zusatzmodule + +| Legacy-Datei | Einordnung | SaaS-Strategie | +| --- | --- | --- | +| `legacy-app/csvupload.php` | operativer CSV-Sonderimport | optionales Import-Modul | +| `legacy-app/exportKaffeeliste.php` | PDF-Export fuer Papierprozess | optionales Export-Modul | +| `legacy-app/mailversenden.php` | Sammelbenachrichtigungen | Notifications | +| `legacy-app/jahresauswertung.php` | Sonderprozess | spaeter oder Entfall | +| `legacy-app/umfrage.php` | Zusatzfunktion | optionales Survey-Modul | +| `legacy-app/umfrageergebnisse.php` | Zusatzfunktion | optionales Survey-Modul | +| `legacy-app/mailausgebe.php` | Hilfs-/Debugseite | Entfall | + +## Datenmodell + +| Legacy-Tabelle | SaaS-Ziel | +| --- | --- | +| `kl_Mitarbeiter` | `users`, `tenant_users`, `members` | +| `kl_Kaffeeverbrauch` | `coffee_entries`, `ledger_entries` | +| `kl_Einzahlungen` | `payment_entries`, `ledger_entries` | +| `kl_hinweise` | `announcements` | +| `kl_config` | tenantbezogene Einstellungen und Feature-Flags | + +## Prioritaet Fuer Die Umsetzung + +1. Dashboard, Members, Ledger und Payments funktional schliessen. +2. Content und Hinweise tenantfaehig machen. +3. Importe, Exporte und Notifications als Backoffice-Module ergaenzen. +4. Surveys und Sonderprozesse nur bei echtem Bedarf uebernehmen. diff --git a/DataTables/datatables.css b/legacy-app/DataTables/datatables.css similarity index 100% rename from DataTables/datatables.css rename to legacy-app/DataTables/datatables.css diff --git a/DataTables/datatables.js b/legacy-app/DataTables/datatables.js similarity index 100% rename from DataTables/datatables.js rename to legacy-app/DataTables/datatables.js diff --git a/DataTables/datatables.min.css b/legacy-app/DataTables/datatables.min.css similarity index 100% rename from DataTables/datatables.min.css rename to legacy-app/DataTables/datatables.min.css diff --git a/DataTables/datatables.min.js b/legacy-app/DataTables/datatables.min.js similarity index 100% rename from DataTables/datatables.min.js rename to legacy-app/DataTables/datatables.min.js diff --git a/PHPMailer/COMMITMENT b/legacy-app/PHPMailer/COMMITMENT similarity index 100% rename from PHPMailer/COMMITMENT rename to legacy-app/PHPMailer/COMMITMENT diff --git a/PHPMailer/LICENSE b/legacy-app/PHPMailer/LICENSE similarity index 100% rename from PHPMailer/LICENSE rename to legacy-app/PHPMailer/LICENSE diff --git a/PHPMailer/composer.json b/legacy-app/PHPMailer/composer.json similarity index 100% rename from PHPMailer/composer.json rename to legacy-app/PHPMailer/composer.json diff --git a/PHPMailer/get_oauth_token.php b/legacy-app/PHPMailer/get_oauth_token.php similarity index 100% rename from PHPMailer/get_oauth_token.php rename to legacy-app/PHPMailer/get_oauth_token.php diff --git a/legacy-app/README.md b/legacy-app/README.md new file mode 100644 index 0000000..3c60abc --- /dev/null +++ b/legacy-app/README.md @@ -0,0 +1,25 @@ +# Legacy App Archiv + +Dieses Verzeichnis enthaelt den bisherigen PHP-Bestand der Kaffeeliste. + +Es bleibt aus drei Gruenden im Repository: + +- als Referenz fuer die bestehende Fachlogik +- als Quelle fuer Datenmigrationen in die SaaS-Version +- als Rueckfalloption waehrend der Uebergangsphase + +## Wichtige Legacy-Bereiche + +- `index.php`: persoenliches Dashboard +- `stricheintragen.php`: Sammelerfassung fuer Kaffee-Striche +- `einzahlung.php`: Sammelerfassung fuer Einzahlungen +- `kaffeeliste.php`: operative Gesamtuebersicht +- `mitarbeiterverwalten.php`: Mitglieder- und Rollenpflege +- `letzteneintraege.php`: Korrektur letzter Buchungen +- `hinweise.php`: Banner/Hinweise + +## Umgang Mit Dem Archiv + +- Keine neuen Produktfunktionen mehr hier entwickeln. +- Nur noch fuer Referenz, Datenabgleich oder Notfallbetrieb verwenden. +- Neue Arbeit findet ausschliesslich in `../saas-app/` und `../docs/` statt. diff --git a/TCPDF/tcpdf.php b/legacy-app/TCPDF/tcpdf.php similarity index 100% rename from TCPDF/tcpdf.php rename to legacy-app/TCPDF/tcpdf.php diff --git a/TCPDF/tcpdf_autoconfig.php b/legacy-app/TCPDF/tcpdf_autoconfig.php similarity index 100% rename from TCPDF/tcpdf_autoconfig.php rename to legacy-app/TCPDF/tcpdf_autoconfig.php diff --git a/TCPDF/tcpdf_barcodes_1d.php b/legacy-app/TCPDF/tcpdf_barcodes_1d.php similarity index 100% rename from TCPDF/tcpdf_barcodes_1d.php rename to legacy-app/TCPDF/tcpdf_barcodes_1d.php diff --git a/TCPDF/tcpdf_barcodes_2d.php b/legacy-app/TCPDF/tcpdf_barcodes_2d.php similarity index 100% rename from TCPDF/tcpdf_barcodes_2d.php rename to legacy-app/TCPDF/tcpdf_barcodes_2d.php diff --git a/TCPDF/tcpdf_import.php b/legacy-app/TCPDF/tcpdf_import.php similarity index 100% rename from TCPDF/tcpdf_import.php rename to legacy-app/TCPDF/tcpdf_import.php diff --git a/assets/css/fontawesome-all.min.css b/legacy-app/assets/css/fontawesome-all.min.css similarity index 100% rename from assets/css/fontawesome-all.min.css rename to legacy-app/assets/css/fontawesome-all.min.css diff --git a/assets/css/main.css b/legacy-app/assets/css/main.css similarity index 99% rename from assets/css/main.css rename to legacy-app/assets/css/main.css index 6ebcbac..a2947d3 100644 --- a/assets/css/main.css +++ b/legacy-app/assets/css/main.css @@ -1,9 +1,9 @@ @import url(fontawesome-all.min.css); @import url("https://fonts.googleapis.com/css?family=Open+Sans:400,600,400italic,600italic|Roboto+Slab:400,700"); -/* - Editorial by HTML5 UP - html5up.net | @ajlkn - Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +/* + Editorial by HTML5 UP + html5up.net | @ajlkn + Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) */ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, diff --git a/assets/js/breakpoints.min.js b/legacy-app/assets/js/breakpoints.min.js similarity index 99% rename from assets/js/breakpoints.min.js rename to legacy-app/assets/js/breakpoints.min.js index 32419cc..e20ae89 100644 --- a/assets/js/breakpoints.min.js +++ b/legacy-app/assets/js/breakpoints.min.js @@ -1,2 +1,2 @@ -/* breakpoints.js v1.0 | @ajlkn | MIT licensed */ +/* breakpoints.js v1.0 | @ajlkn | MIT licensed */ var breakpoints=function(){"use strict";function e(e){t.init(e)}var t={list:null,media:{},events:[],init:function(e){t.list=e,window.addEventListener("resize",t.poll),window.addEventListener("orientationchange",t.poll),window.addEventListener("load",t.poll),window.addEventListener("fullscreenchange",t.poll)},active:function(e){var n,a,s,i,r,d,c;if(!(e in t.media)){if(">="==e.substr(0,2)?(a="gte",n=e.substr(2)):"<="==e.substr(0,2)?(a="lte",n=e.substr(2)):">"==e.substr(0,1)?(a="gt",n=e.substr(1)):"<"==e.substr(0,1)?(a="lt",n=e.substr(1)):"!"==e.substr(0,1)?(a="not",n=e.substr(1)):(a="eq",n=e),n&&n in t.list)if(i=t.list[n],Array.isArray(i)){if(r=parseInt(i[0]),d=parseInt(i[1]),isNaN(r)){if(isNaN(d))return;c=i[1].substr(String(d).length)}else c=i[0].substr(String(r).length);if(isNaN(r))switch(a){case"gte":s="screen";break;case"lte":s="screen and (max-width: "+d+c+")";break;case"gt":s="screen and (min-width: "+(d+1)+c+")";break;case"lt":s="screen and (max-width: -1px)";break;case"not":s="screen and (min-width: "+(d+1)+c+")";break;default:s="screen and (max-width: "+d+c+")"}else if(isNaN(d))switch(a){case"gte":s="screen and (min-width: "+r+c+")";break;case"lte":s="screen";break;case"gt":s="screen and (max-width: -1px)";break;case"lt":s="screen and (max-width: "+(r-1)+c+")";break;case"not":s="screen and (max-width: "+(r-1)+c+")";break;default:s="screen and (min-width: "+r+c+")"}else switch(a){case"gte":s="screen and (min-width: "+r+c+")";break;case"lte":s="screen and (max-width: "+d+c+")";break;case"gt":s="screen and (min-width: "+(d+1)+c+")";break;case"lt":s="screen and (max-width: "+(r-1)+c+")";break;case"not":s="screen and (max-width: "+(r-1)+c+"), screen and (min-width: "+(d+1)+c+")";break;default:s="screen and (min-width: "+r+c+") and (max-width: "+d+c+")"}}else s="("==i.charAt(0)?"screen and "+i:i;t.media[e]=!!s&&s}return t.media[e]!==!1&&window.matchMedia(t.media[e]).matches},on:function(e,n){t.events.push({query:e,handler:n,state:!1}),t.active(e)&&n()},poll:function(){var e,n;for(e=0;elarge', function() { - $sidebar.removeClass('inactive'); - }); - - // Hack: Workaround for Chrome/Android scrollbar position bug. - if (browser.os == 'android' - && browser.name == 'chrome') - $('') - .appendTo($head); - - // Toggle. - $('Toggle') - .appendTo($sidebar) - .on('click', function(event) { - - // Prevent default. - event.preventDefault(); - event.stopPropagation(); - - // Toggle. - $sidebar.toggleClass('inactive'); - - }); - - // Events. - - // Link clicks. - $sidebar.on('click', 'a', function(event) { - - // >large? Bail. - if (breakpoints.active('>large')) - return; - - // Vars. - var $a = $(this), - href = $a.attr('href'), - target = $a.attr('target'); - - // Prevent default. - event.preventDefault(); - event.stopPropagation(); - - // Check URL. - if (!href || href == '#' || href == '') - return; - - // Hide sidebar. - $sidebar.addClass('inactive'); - - // Redirect to href. - setTimeout(function() { - - if (target == '_blank') - window.open(href); - else - window.location.href = href; - - }, 500); - - }); - - // Prevent certain events inside the panel from bubbling. - $sidebar.on('click touchend touchstart touchmove', function(event) { - - // >large? Bail. - if (breakpoints.active('>large')) - return; - - // Prevent propagation. - event.stopPropagation(); - - }); - - // Hide panel on body click/tap. - $body.on('click touchend', function(event) { - - // >large? Bail. - if (breakpoints.active('>large')) - return; - - // Deactivate. - $sidebar.addClass('inactive'); - - }); - - // Scroll lock. - // Note: If you do anything to change the height of the sidebar's content, be sure to - // trigger 'resize.sidebar-lock' on $window so stuff doesn't get out of sync. - - $window.on('load.sidebar-lock', function() { - - var sh, wh, st; - - // Reset scroll position to 0 if it's 1. - if ($window.scrollTop() == 1) - $window.scrollTop(0); - - $window - .on('scroll.sidebar-lock', function() { - - var x, y; - - // <=large? Bail. - if (breakpoints.active('<=large')) { - - $sidebar_inner - .data('locked', 0) - .css('position', '') - .css('top', ''); - - return; - - } - - // Calculate positions. - x = Math.max(sh - wh, 0); - y = Math.max(0, $window.scrollTop() - x); - - // Lock/unlock. - if ($sidebar_inner.data('locked') == 1) { - - if (y <= 0) - $sidebar_inner - .data('locked', 0) - .css('position', '') - .css('top', ''); - else - $sidebar_inner - .css('top', -1 * x); - - } - else { - - if (y > 0) - $sidebar_inner - .data('locked', 1) - .css('position', 'fixed') - .css('top', -1 * x); - - } - - }) - .on('resize.sidebar-lock', function() { - - // Calculate heights. - wh = $window.height(); - sh = $sidebar_inner.outerHeight() + 30; - - // Trigger scroll. - $window.trigger('scroll.sidebar-lock'); - - }) - .trigger('resize.sidebar-lock'); - - }); - - // Menu. - var $menu = $('#menu'), - $menu_openers = $menu.children('ul').find('.opener'); - - // Openers. - $menu_openers.each(function() { - - var $this = $(this); - - $this.on('click', function(event) { - - // Prevent default. - event.preventDefault(); - - // Toggle. - $menu_openers.not($this).removeClass('active'); - $this.toggleClass('active'); - - // Trigger resize (sidebar lock). - $window.triggerHandler('resize.sidebar-lock'); - - }); - - }); - +/* + Editorial by HTML5 UP + html5up.net | @ajlkn + Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +*/ + +(function($) { + + var $window = $(window), + $head = $('head'), + $body = $('body'); + + // Breakpoints. + breakpoints({ + xlarge: [ '1281px', '1680px' ], + large: [ '981px', '1280px' ], + medium: [ '737px', '980px' ], + small: [ '481px', '736px' ], + xsmall: [ '361px', '480px' ], + xxsmall: [ null, '360px' ], + 'xlarge-to-max': '(min-width: 1681px)', + 'small-to-xlarge': '(min-width: 481px) and (max-width: 1680px)' + }); + + // Stops animations/transitions until the page has ... + + // ... loaded. + $window.on('load', function() { + window.setTimeout(function() { + $body.removeClass('is-preload'); + }, 100); + }); + + // ... stopped resizing. + var resizeTimeout; + + $window.on('resize', function() { + + // Mark as resizing. + $body.addClass('is-resizing'); + + // Unmark after delay. + clearTimeout(resizeTimeout); + + resizeTimeout = setTimeout(function() { + $body.removeClass('is-resizing'); + }, 100); + + }); + + // Fixes. + + // Object fit images. + if (!browser.canUse('object-fit') + || browser.name == 'safari') + $('.image.object').each(function() { + + var $this = $(this), + $img = $this.children('img'); + + // Hide original image. + $img.css('opacity', '0'); + + // Set background. + $this + .css('background-image', 'url("' + $img.attr('src') + '")') + .css('background-size', $img.css('object-fit') ? $img.css('object-fit') : 'cover') + .css('background-position', $img.css('object-position') ? $img.css('object-position') : 'center'); + + }); + + // Sidebar. + var $sidebar = $('#sidebar'), + $sidebar_inner = $sidebar.children('.inner'); + + // Inactive by default on <= large. + breakpoints.on('<=large', function() { + $sidebar.addClass('inactive'); + }); + + breakpoints.on('>large', function() { + $sidebar.removeClass('inactive'); + }); + + // Hack: Workaround for Chrome/Android scrollbar position bug. + if (browser.os == 'android' + && browser.name == 'chrome') + $('') + .appendTo($head); + + // Toggle. + $('Toggle') + .appendTo($sidebar) + .on('click', function(event) { + + // Prevent default. + event.preventDefault(); + event.stopPropagation(); + + // Toggle. + $sidebar.toggleClass('inactive'); + + }); + + // Events. + + // Link clicks. + $sidebar.on('click', 'a', function(event) { + + // >large? Bail. + if (breakpoints.active('>large')) + return; + + // Vars. + var $a = $(this), + href = $a.attr('href'), + target = $a.attr('target'); + + // Prevent default. + event.preventDefault(); + event.stopPropagation(); + + // Check URL. + if (!href || href == '#' || href == '') + return; + + // Hide sidebar. + $sidebar.addClass('inactive'); + + // Redirect to href. + setTimeout(function() { + + if (target == '_blank') + window.open(href); + else + window.location.href = href; + + }, 500); + + }); + + // Prevent certain events inside the panel from bubbling. + $sidebar.on('click touchend touchstart touchmove', function(event) { + + // >large? Bail. + if (breakpoints.active('>large')) + return; + + // Prevent propagation. + event.stopPropagation(); + + }); + + // Hide panel on body click/tap. + $body.on('click touchend', function(event) { + + // >large? Bail. + if (breakpoints.active('>large')) + return; + + // Deactivate. + $sidebar.addClass('inactive'); + + }); + + // Scroll lock. + // Note: If you do anything to change the height of the sidebar's content, be sure to + // trigger 'resize.sidebar-lock' on $window so stuff doesn't get out of sync. + + $window.on('load.sidebar-lock', function() { + + var sh, wh, st; + + // Reset scroll position to 0 if it's 1. + if ($window.scrollTop() == 1) + $window.scrollTop(0); + + $window + .on('scroll.sidebar-lock', function() { + + var x, y; + + // <=large? Bail. + if (breakpoints.active('<=large')) { + + $sidebar_inner + .data('locked', 0) + .css('position', '') + .css('top', ''); + + return; + + } + + // Calculate positions. + x = Math.max(sh - wh, 0); + y = Math.max(0, $window.scrollTop() - x); + + // Lock/unlock. + if ($sidebar_inner.data('locked') == 1) { + + if (y <= 0) + $sidebar_inner + .data('locked', 0) + .css('position', '') + .css('top', ''); + else + $sidebar_inner + .css('top', -1 * x); + + } + else { + + if (y > 0) + $sidebar_inner + .data('locked', 1) + .css('position', 'fixed') + .css('top', -1 * x); + + } + + }) + .on('resize.sidebar-lock', function() { + + // Calculate heights. + wh = $window.height(); + sh = $sidebar_inner.outerHeight() + 30; + + // Trigger scroll. + $window.trigger('scroll.sidebar-lock'); + + }) + .trigger('resize.sidebar-lock'); + + }); + + // Menu. + var $menu = $('#menu'), + $menu_openers = $menu.children('ul').find('.opener'); + + // Openers. + $menu_openers.each(function() { + + var $this = $(this); + + $this.on('click', function(event) { + + // Prevent default. + event.preventDefault(); + + // Toggle. + $menu_openers.not($this).removeClass('active'); + $this.toggleClass('active'); + + // Trigger resize (sidebar lock). + $window.triggerHandler('resize.sidebar-lock'); + + }); + + }); + })(jQuery); \ No newline at end of file diff --git a/assets/js/util.js b/legacy-app/assets/js/util.js similarity index 95% rename from assets/js/util.js rename to legacy-app/assets/js/util.js index bdb8e9f..ecf7b37 100644 --- a/assets/js/util.js +++ b/legacy-app/assets/js/util.js @@ -1,587 +1,587 @@ -(function($) { - - /** - * Generate an indented list of links from a nav. Meant for use with panel(). - * @return {jQuery} jQuery object. - */ - $.fn.navList = function() { - - var $this = $(this); - $a = $this.find('a'), - b = []; - - $a.each(function() { - - var $this = $(this), - indent = Math.max(0, $this.parents('li').length - 1), - href = $this.attr('href'), - target = $this.attr('target'); - - b.push( - '' + - '' + - $this.text() + - '' - ); - - }); - - return b.join(''); - - }; - - /** - * Panel-ify an element. - * @param {object} userConfig User config. - * @return {jQuery} jQuery object. - */ - $.fn.panel = function(userConfig) { - - // No elements? - if (this.length == 0) - return $this; - - // Multiple elements? - if (this.length > 1) { - - for (var i=0; i < this.length; i++) - $(this[i]).panel(userConfig); - - return $this; - - } - - // Vars. - var $this = $(this), - $body = $('body'), - $window = $(window), - id = $this.attr('id'), - config; - - // Config. - config = $.extend({ - - // Delay. - delay: 0, - - // Hide panel on link click. - hideOnClick: false, - - // Hide panel on escape keypress. - hideOnEscape: false, - - // Hide panel on swipe. - hideOnSwipe: false, - - // Reset scroll position on hide. - resetScroll: false, - - // Reset forms on hide. - resetForms: false, - - // Side of viewport the panel will appear. - side: null, - - // Target element for "class". - target: $this, - - // Class to toggle. - visibleClass: 'visible' - - }, userConfig); - - // Expand "target" if it's not a jQuery object already. - if (typeof config.target != 'jQuery') - config.target = $(config.target); - - // Panel. - - // Methods. - $this._hide = function(event) { - - // Already hidden? Bail. - if (!config.target.hasClass(config.visibleClass)) - return; - - // If an event was provided, cancel it. - if (event) { - - event.preventDefault(); - event.stopPropagation(); - - } - - // Hide. - config.target.removeClass(config.visibleClass); - - // Post-hide stuff. - window.setTimeout(function() { - - // Reset scroll position. - if (config.resetScroll) - $this.scrollTop(0); - - // Reset forms. - if (config.resetForms) - $this.find('form').each(function() { - this.reset(); - }); - - }, config.delay); - - }; - - // Vendor fixes. - $this - .css('-ms-overflow-style', '-ms-autohiding-scrollbar') - .css('-webkit-overflow-scrolling', 'touch'); - - // Hide on click. - if (config.hideOnClick) { - - $this.find('a') - .css('-webkit-tap-highlight-color', 'rgba(0,0,0,0)'); - - $this - .on('click', 'a', function(event) { - - var $a = $(this), - href = $a.attr('href'), - target = $a.attr('target'); - - if (!href || href == '#' || href == '' || href == '#' + id) - return; - - // Cancel original event. - event.preventDefault(); - event.stopPropagation(); - - // Hide panel. - $this._hide(); - - // Redirect to href. - window.setTimeout(function() { - - if (target == '_blank') - window.open(href); - else - window.location.href = href; - - }, config.delay + 10); - - }); - - } - - // Event: Touch stuff. - $this.on('touchstart', function(event) { - - $this.touchPosX = event.originalEvent.touches[0].pageX; - $this.touchPosY = event.originalEvent.touches[0].pageY; - - }) - - $this.on('touchmove', function(event) { - - if ($this.touchPosX === null - || $this.touchPosY === null) - return; - - var diffX = $this.touchPosX - event.originalEvent.touches[0].pageX, - diffY = $this.touchPosY - event.originalEvent.touches[0].pageY, - th = $this.outerHeight(), - ts = ($this.get(0).scrollHeight - $this.scrollTop()); - - // Hide on swipe? - if (config.hideOnSwipe) { - - var result = false, - boundary = 20, - delta = 50; - - switch (config.side) { - - case 'left': - result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX > delta); - break; - - case 'right': - result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX < (-1 * delta)); - break; - - case 'top': - result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY > delta); - break; - - case 'bottom': - result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY < (-1 * delta)); - break; - - default: - break; - - } - - if (result) { - - $this.touchPosX = null; - $this.touchPosY = null; - $this._hide(); - - return false; - - } - - } - - // Prevent vertical scrolling past the top or bottom. - if (($this.scrollTop() < 0 && diffY < 0) - || (ts > (th - 2) && ts < (th + 2) && diffY > 0)) { - - event.preventDefault(); - event.stopPropagation(); - - } - - }); - - // Event: Prevent certain events inside the panel from bubbling. - $this.on('click touchend touchstart touchmove', function(event) { - event.stopPropagation(); - }); - - // Event: Hide panel if a child anchor tag pointing to its ID is clicked. - $this.on('click', 'a[href="#' + id + '"]', function(event) { - - event.preventDefault(); - event.stopPropagation(); - - config.target.removeClass(config.visibleClass); - - }); - - // Body. - - // Event: Hide panel on body click/tap. - $body.on('click touchend', function(event) { - $this._hide(event); - }); - - // Event: Toggle. - $body.on('click', 'a[href="#' + id + '"]', function(event) { - - event.preventDefault(); - event.stopPropagation(); - - config.target.toggleClass(config.visibleClass); - - }); - - // Window. - - // Event: Hide on ESC. - if (config.hideOnEscape) - $window.on('keydown', function(event) { - - if (event.keyCode == 27) - $this._hide(event); - - }); - - return $this; - - }; - - /** - * Apply "placeholder" attribute polyfill to one or more forms. - * @return {jQuery} jQuery object. - */ - $.fn.placeholder = function() { - - // Browser natively supports placeholders? Bail. - if (typeof (document.createElement('input')).placeholder != 'undefined') - return $(this); - - // No elements? - if (this.length == 0) - return $this; - - // Multiple elements? - if (this.length > 1) { - - for (var i=0; i < this.length; i++) - $(this[i]).placeholder(); - - return $this; - - } - - // Vars. - var $this = $(this); - - // Text, TextArea. - $this.find('input[type=text],textarea') - .each(function() { - - var i = $(this); - - if (i.val() == '' - || i.val() == i.attr('placeholder')) - i - .addClass('polyfill-placeholder') - .val(i.attr('placeholder')); - - }) - .on('blur', function() { - - var i = $(this); - - if (i.attr('name').match(/-polyfill-field$/)) - return; - - if (i.val() == '') - i - .addClass('polyfill-placeholder') - .val(i.attr('placeholder')); - - }) - .on('focus', function() { - - var i = $(this); - - if (i.attr('name').match(/-polyfill-field$/)) - return; - - if (i.val() == i.attr('placeholder')) - i - .removeClass('polyfill-placeholder') - .val(''); - - }); - - // Password. - $this.find('input[type=password]') - .each(function() { - - var i = $(this); - var x = $( - $('
') - .append(i.clone()) - .remove() - .html() - .replace(/type="password"/i, 'type="text"') - .replace(/type=password/i, 'type=text') - ); - - if (i.attr('id') != '') - x.attr('id', i.attr('id') + '-polyfill-field'); - - if (i.attr('name') != '') - x.attr('name', i.attr('name') + '-polyfill-field'); - - x.addClass('polyfill-placeholder') - .val(x.attr('placeholder')).insertAfter(i); - - if (i.val() == '') - i.hide(); - else - x.hide(); - - i - .on('blur', function(event) { - - event.preventDefault(); - - var x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]'); - - if (i.val() == '') { - - i.hide(); - x.show(); - - } - - }); - - x - .on('focus', function(event) { - - event.preventDefault(); - - var i = x.parent().find('input[name=' + x.attr('name').replace('-polyfill-field', '') + ']'); - - x.hide(); - - i - .show() - .focus(); - - }) - .on('keypress', function(event) { - - event.preventDefault(); - x.val(''); - - }); - - }); - - // Events. - $this - .on('submit', function() { - - $this.find('input[type=text],input[type=password],textarea') - .each(function(event) { - - var i = $(this); - - if (i.attr('name').match(/-polyfill-field$/)) - i.attr('name', ''); - - if (i.val() == i.attr('placeholder')) { - - i.removeClass('polyfill-placeholder'); - i.val(''); - - } - - }); - - }) - .on('reset', function(event) { - - event.preventDefault(); - - $this.find('select') - .val($('option:first').val()); - - $this.find('input,textarea') - .each(function() { - - var i = $(this), - x; - - i.removeClass('polyfill-placeholder'); - - switch (this.type) { - - case 'submit': - case 'reset': - break; - - case 'password': - i.val(i.attr('defaultValue')); - - x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]'); - - if (i.val() == '') { - i.hide(); - x.show(); - } - else { - i.show(); - x.hide(); - } - - break; - - case 'checkbox': - case 'radio': - i.attr('checked', i.attr('defaultValue')); - break; - - case 'text': - case 'textarea': - i.val(i.attr('defaultValue')); - - if (i.val() == '') { - i.addClass('polyfill-placeholder'); - i.val(i.attr('placeholder')); - } - - break; - - default: - i.val(i.attr('defaultValue')); - break; - - } - }); - - }); - - return $this; - - }; - - /** - * Moves elements to/from the first positions of their respective parents. - * @param {jQuery} $elements Elements (or selector) to move. - * @param {bool} condition If true, moves elements to the top. Otherwise, moves elements back to their original locations. - */ - $.prioritize = function($elements, condition) { - - var key = '__prioritize'; - - // Expand $elements if it's not already a jQuery object. - if (typeof $elements != 'jQuery') - $elements = $($elements); - - // Step through elements. - $elements.each(function() { - - var $e = $(this), $p, - $parent = $e.parent(); - - // No parent? Bail. - if ($parent.length == 0) - return; - - // Not moved? Move it. - if (!$e.data(key)) { - - // Condition is false? Bail. - if (!condition) - return; - - // Get placeholder (which will serve as our point of reference for when this element needs to move back). - $p = $e.prev(); - - // Couldn't find anything? Means this element's already at the top, so bail. - if ($p.length == 0) - return; - - // Move element to top of parent. - $e.prependTo($parent); - - // Mark element as moved. - $e.data(key, $p); - - } - - // Moved already? - else { - - // Condition is true? Bail. - if (condition) - return; - - $p = $e.data(key); - - // Move element back to its original location (using our placeholder). - $e.insertAfter($p); - - // Unmark element as moved. - $e.removeData(key); - - } - - }); - - }; - +(function($) { + + /** + * Generate an indented list of links from a nav. Meant for use with panel(). + * @return {jQuery} jQuery object. + */ + $.fn.navList = function() { + + var $this = $(this); + $a = $this.find('a'), + b = []; + + $a.each(function() { + + var $this = $(this), + indent = Math.max(0, $this.parents('li').length - 1), + href = $this.attr('href'), + target = $this.attr('target'); + + b.push( + '' + + '' + + $this.text() + + '' + ); + + }); + + return b.join(''); + + }; + + /** + * Panel-ify an element. + * @param {object} userConfig User config. + * @return {jQuery} jQuery object. + */ + $.fn.panel = function(userConfig) { + + // No elements? + if (this.length == 0) + return $this; + + // Multiple elements? + if (this.length > 1) { + + for (var i=0; i < this.length; i++) + $(this[i]).panel(userConfig); + + return $this; + + } + + // Vars. + var $this = $(this), + $body = $('body'), + $window = $(window), + id = $this.attr('id'), + config; + + // Config. + config = $.extend({ + + // Delay. + delay: 0, + + // Hide panel on link click. + hideOnClick: false, + + // Hide panel on escape keypress. + hideOnEscape: false, + + // Hide panel on swipe. + hideOnSwipe: false, + + // Reset scroll position on hide. + resetScroll: false, + + // Reset forms on hide. + resetForms: false, + + // Side of viewport the panel will appear. + side: null, + + // Target element for "class". + target: $this, + + // Class to toggle. + visibleClass: 'visible' + + }, userConfig); + + // Expand "target" if it's not a jQuery object already. + if (typeof config.target != 'jQuery') + config.target = $(config.target); + + // Panel. + + // Methods. + $this._hide = function(event) { + + // Already hidden? Bail. + if (!config.target.hasClass(config.visibleClass)) + return; + + // If an event was provided, cancel it. + if (event) { + + event.preventDefault(); + event.stopPropagation(); + + } + + // Hide. + config.target.removeClass(config.visibleClass); + + // Post-hide stuff. + window.setTimeout(function() { + + // Reset scroll position. + if (config.resetScroll) + $this.scrollTop(0); + + // Reset forms. + if (config.resetForms) + $this.find('form').each(function() { + this.reset(); + }); + + }, config.delay); + + }; + + // Vendor fixes. + $this + .css('-ms-overflow-style', '-ms-autohiding-scrollbar') + .css('-webkit-overflow-scrolling', 'touch'); + + // Hide on click. + if (config.hideOnClick) { + + $this.find('a') + .css('-webkit-tap-highlight-color', 'rgba(0,0,0,0)'); + + $this + .on('click', 'a', function(event) { + + var $a = $(this), + href = $a.attr('href'), + target = $a.attr('target'); + + if (!href || href == '#' || href == '' || href == '#' + id) + return; + + // Cancel original event. + event.preventDefault(); + event.stopPropagation(); + + // Hide panel. + $this._hide(); + + // Redirect to href. + window.setTimeout(function() { + + if (target == '_blank') + window.open(href); + else + window.location.href = href; + + }, config.delay + 10); + + }); + + } + + // Event: Touch stuff. + $this.on('touchstart', function(event) { + + $this.touchPosX = event.originalEvent.touches[0].pageX; + $this.touchPosY = event.originalEvent.touches[0].pageY; + + }) + + $this.on('touchmove', function(event) { + + if ($this.touchPosX === null + || $this.touchPosY === null) + return; + + var diffX = $this.touchPosX - event.originalEvent.touches[0].pageX, + diffY = $this.touchPosY - event.originalEvent.touches[0].pageY, + th = $this.outerHeight(), + ts = ($this.get(0).scrollHeight - $this.scrollTop()); + + // Hide on swipe? + if (config.hideOnSwipe) { + + var result = false, + boundary = 20, + delta = 50; + + switch (config.side) { + + case 'left': + result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX > delta); + break; + + case 'right': + result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX < (-1 * delta)); + break; + + case 'top': + result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY > delta); + break; + + case 'bottom': + result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY < (-1 * delta)); + break; + + default: + break; + + } + + if (result) { + + $this.touchPosX = null; + $this.touchPosY = null; + $this._hide(); + + return false; + + } + + } + + // Prevent vertical scrolling past the top or bottom. + if (($this.scrollTop() < 0 && diffY < 0) + || (ts > (th - 2) && ts < (th + 2) && diffY > 0)) { + + event.preventDefault(); + event.stopPropagation(); + + } + + }); + + // Event: Prevent certain events inside the panel from bubbling. + $this.on('click touchend touchstart touchmove', function(event) { + event.stopPropagation(); + }); + + // Event: Hide panel if a child anchor tag pointing to its ID is clicked. + $this.on('click', 'a[href="#' + id + '"]', function(event) { + + event.preventDefault(); + event.stopPropagation(); + + config.target.removeClass(config.visibleClass); + + }); + + // Body. + + // Event: Hide panel on body click/tap. + $body.on('click touchend', function(event) { + $this._hide(event); + }); + + // Event: Toggle. + $body.on('click', 'a[href="#' + id + '"]', function(event) { + + event.preventDefault(); + event.stopPropagation(); + + config.target.toggleClass(config.visibleClass); + + }); + + // Window. + + // Event: Hide on ESC. + if (config.hideOnEscape) + $window.on('keydown', function(event) { + + if (event.keyCode == 27) + $this._hide(event); + + }); + + return $this; + + }; + + /** + * Apply "placeholder" attribute polyfill to one or more forms. + * @return {jQuery} jQuery object. + */ + $.fn.placeholder = function() { + + // Browser natively supports placeholders? Bail. + if (typeof (document.createElement('input')).placeholder != 'undefined') + return $(this); + + // No elements? + if (this.length == 0) + return $this; + + // Multiple elements? + if (this.length > 1) { + + for (var i=0; i < this.length; i++) + $(this[i]).placeholder(); + + return $this; + + } + + // Vars. + var $this = $(this); + + // Text, TextArea. + $this.find('input[type=text],textarea') + .each(function() { + + var i = $(this); + + if (i.val() == '' + || i.val() == i.attr('placeholder')) + i + .addClass('polyfill-placeholder') + .val(i.attr('placeholder')); + + }) + .on('blur', function() { + + var i = $(this); + + if (i.attr('name').match(/-polyfill-field$/)) + return; + + if (i.val() == '') + i + .addClass('polyfill-placeholder') + .val(i.attr('placeholder')); + + }) + .on('focus', function() { + + var i = $(this); + + if (i.attr('name').match(/-polyfill-field$/)) + return; + + if (i.val() == i.attr('placeholder')) + i + .removeClass('polyfill-placeholder') + .val(''); + + }); + + // Password. + $this.find('input[type=password]') + .each(function() { + + var i = $(this); + var x = $( + $('
') + .append(i.clone()) + .remove() + .html() + .replace(/type="password"/i, 'type="text"') + .replace(/type=password/i, 'type=text') + ); + + if (i.attr('id') != '') + x.attr('id', i.attr('id') + '-polyfill-field'); + + if (i.attr('name') != '') + x.attr('name', i.attr('name') + '-polyfill-field'); + + x.addClass('polyfill-placeholder') + .val(x.attr('placeholder')).insertAfter(i); + + if (i.val() == '') + i.hide(); + else + x.hide(); + + i + .on('blur', function(event) { + + event.preventDefault(); + + var x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]'); + + if (i.val() == '') { + + i.hide(); + x.show(); + + } + + }); + + x + .on('focus', function(event) { + + event.preventDefault(); + + var i = x.parent().find('input[name=' + x.attr('name').replace('-polyfill-field', '') + ']'); + + x.hide(); + + i + .show() + .focus(); + + }) + .on('keypress', function(event) { + + event.preventDefault(); + x.val(''); + + }); + + }); + + // Events. + $this + .on('submit', function() { + + $this.find('input[type=text],input[type=password],textarea') + .each(function(event) { + + var i = $(this); + + if (i.attr('name').match(/-polyfill-field$/)) + i.attr('name', ''); + + if (i.val() == i.attr('placeholder')) { + + i.removeClass('polyfill-placeholder'); + i.val(''); + + } + + }); + + }) + .on('reset', function(event) { + + event.preventDefault(); + + $this.find('select') + .val($('option:first').val()); + + $this.find('input,textarea') + .each(function() { + + var i = $(this), + x; + + i.removeClass('polyfill-placeholder'); + + switch (this.type) { + + case 'submit': + case 'reset': + break; + + case 'password': + i.val(i.attr('defaultValue')); + + x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]'); + + if (i.val() == '') { + i.hide(); + x.show(); + } + else { + i.show(); + x.hide(); + } + + break; + + case 'checkbox': + case 'radio': + i.attr('checked', i.attr('defaultValue')); + break; + + case 'text': + case 'textarea': + i.val(i.attr('defaultValue')); + + if (i.val() == '') { + i.addClass('polyfill-placeholder'); + i.val(i.attr('placeholder')); + } + + break; + + default: + i.val(i.attr('defaultValue')); + break; + + } + }); + + }); + + return $this; + + }; + + /** + * Moves elements to/from the first positions of their respective parents. + * @param {jQuery} $elements Elements (or selector) to move. + * @param {bool} condition If true, moves elements to the top. Otherwise, moves elements back to their original locations. + */ + $.prioritize = function($elements, condition) { + + var key = '__prioritize'; + + // Expand $elements if it's not already a jQuery object. + if (typeof $elements != 'jQuery') + $elements = $($elements); + + // Step through elements. + $elements.each(function() { + + var $e = $(this), $p, + $parent = $e.parent(); + + // No parent? Bail. + if ($parent.length == 0) + return; + + // Not moved? Move it. + if (!$e.data(key)) { + + // Condition is false? Bail. + if (!condition) + return; + + // Get placeholder (which will serve as our point of reference for when this element needs to move back). + $p = $e.prev(); + + // Couldn't find anything? Means this element's already at the top, so bail. + if ($p.length == 0) + return; + + // Move element to top of parent. + $e.prependTo($parent); + + // Mark element as moved. + $e.data(key, $p); + + } + + // Moved already? + else { + + // Condition is true? Bail. + if (condition) + return; + + $p = $e.data(key); + + // Move element back to its original location (using our placeholder). + $e.insertAfter($p); + + // Unmark element as moved. + $e.removeData(key); + + } + + }); + + }; + })(jQuery); \ No newline at end of file diff --git a/config.php b/legacy-app/config.php similarity index 96% rename from config.php rename to legacy-app/config.php index b12e460..8e86c6d 100644 --- a/config.php +++ b/legacy-app/config.php @@ -1,39 +1,39 @@ - "", - "Uid" => "", - "PWD" => "", - "TrustServerCertificate"=>true -); - -$conn = sqlsrv_connect($serverName, $connectionOptions); -// Überprüfen der Verbindung -if (!$conn) { - die(print_r(sqlsrv_errors(), true)); -} -?> + "", + "Uid" => "", + "PWD" => "", + "TrustServerCertificate"=>true +); + +$conn = sqlsrv_connect($serverName, $connectionOptions); +// Überprüfen der Verbindung +if (!$conn) { + die(print_r(sqlsrv_errors(), true)); +} +?> diff --git a/csvupload.php b/legacy-app/csvupload.php similarity index 96% rename from csvupload.php rename to legacy-app/csvupload.php index a006ebd..ea0c001 100644 --- a/csvupload.php +++ b/legacy-app/csvupload.php @@ -1,218 +1,218 @@ - - - - - + + + + + \ No newline at end of file diff --git a/einzahlung.php b/legacy-app/einzahlung.php similarity index 96% rename from einzahlung.php rename to legacy-app/einzahlung.php index 0290f90..d0f282e 100644 --- a/einzahlung.php +++ b/legacy-app/einzahlung.php @@ -1,152 +1,152 @@ - - - - - + + + + + \ No newline at end of file diff --git a/exportKaffeeliste.php b/legacy-app/exportKaffeeliste.php similarity index 97% rename from exportKaffeeliste.php rename to legacy-app/exportKaffeeliste.php index 767d101..6d65bba 100644 --- a/exportKaffeeliste.php +++ b/legacy-app/exportKaffeeliste.php @@ -1,224 +1,224 @@ -= DATEADD(DAY, -100, (SELECT MAX(Datum) FROM kl_Kaffeeverbrauch WHERE Datum < CAST(GETDATE() AS DATE))) AND M.aktiv = 1 -GROUP BY M.MitarbeiterID, M.Name, M.Email -HAVING SUM(V.AnzahlStriche) >= 10 -ORDER BY Name;"; -$stmtMitglieder = sqlsrv_query($conn, $sqlMitglieder); - -// Kosten pro Strich auslesen -$sqlKostenproStrich = "SELECT KostenproStrich FROM kl_config "; -$stmtKostenproStrich = sqlsrv_query($conn, $sqlKostenproStrich); -$row = sqlsrv_fetch_array($stmtKostenproStrich, SQLSRV_FETCH_ASSOC); -$KostenproStrichtemp = $row["KostenproStrich"]; -$KostenproStrich = number_format($KostenproStrichtemp, 2, ',', '.'); - -// TCPDF-Bibliothek einbinden -require_once('tcpdf/tcpdf.php'); - -class MyCustomPDFWithWatermark extends TCPDF { - public function Header() { - // Get the current page break margin - $bMargin = $this->getBreakMargin(); - - // Get current auto-page-break mode - $auto_page_break = $this->AutoPageBreak; - - // Disable auto-page-break - $this->SetAutoPageBreak(false, 0); - - // Define the path to the image that you want to use as watermark. - $img_file = './watermark.jpg'; - $this->SetAlpha(0.35); - // Render the image - $this->Image($img_file, 0, 0, 223, 280, '', '', '', false, 300, '', false, false, 0); - $this->SetAlpha(1); - // Restore the auto-page-break status - $this->SetAutoPageBreak($auto_page_break, $bMargin); - - // Set the starting point for the page content - $this->setPageMark(); - } -} - - - -// PDF-Objekt erstellen -#$pdf = new TCPDF(); -$pdf = new MyCustomPDFWithWatermark(PDF_PAGE_ORIENTATION, 'mm', 'A4', true, 'UTF-8', false); - - -// PDF-Header setzen -$pdf->SetHeaderData("", 0, "Kaffeestrichliste", ""); - -// PDF-Header und Footer auf jeder Seite anzeigen -#$pdf->SetPrintHeader(false); -$pdf->SetPrintFooter(false); -$pdf->SetMargins('5', '5', '5'); -$pdf->SetAutoPageBreak(TRUE, 5); -// Seitenformat setzen -#$pdf->SetFormat('A4', 'portrait'); -$pdf->SetFont('helvetica', '', 9.5); -// PDF-Inhalt starten -$pdf->AddPage(); - - - -// Tabelle erstellen -$html = ' - - - - - - - - - - '; -$y=1; -while ($row = sqlsrv_fetch_array($stmtMitglieder, SQLSRV_FETCH_ASSOC)) { - $mitarbeiterID = $row['MitarbeiterID']; - $name = $row['Name']; - $email = $row['Email']; - $y++; - $gesamteinzahlungen = berechneGesamteinzahlungen($mitarbeiterID, $conn); - $gesamtausgaben = berechneGesamtausgabe($mitarbeiterID, $conn); - $differenztemp = $gesamteinzahlungen - $gesamtausgaben; - $differenz = number_format($differenztemp, 2, ',', '.'); - $html .= ''; - $html .= ""; - #$pdf->writeHTML($html, true, false, true, false, ''); - #$html = ""; - if($differenztemp < -10.00){ - $html .= ''; - }elseif($differenztemp > 5.00){ - $html .= ''; - }else{ - $html .= ""; - } - $html .= ""; - $html .= ""; -} - -for ($i = $y; $i < 64; $i++) { - $html .= ''; -} -$html .= ''; -$html .= '
Kaffeeliste - Vieltrinker1 Strich = ' . $KostenproStrich . '€. Bitte bezahlen bei 10 € zahlen. ' . date("d.m.Y H:s") . '
NameGuthabenStriche
{$name}' .$differenz . ' €' .$differenz . ' €{$differenz} €
   
  Rückseite beachten!
'; - - -#echo $html; -// Tabelle ins PDF einfügen -$pdf->writeHTML($html, true, false, true, false, ''); - - -// PDF-Inhalt starten -$pdf->AddPage(); - -// Mitglieder aus der Datenbank abrufen -$sqlMitglieder = " -SELECT M.MitarbeiterID, M.Name, M.Email -FROM kl_Mitarbeiter M -LEFT JOIN kl_Kaffeeverbrauch V ON M.MitarbeiterID = V.MitarbeiterID AND V.Datum >= DATEADD(DAY, -100, (SELECT MAX(Datum) FROM kl_Kaffeeverbrauch WHERE Datum < CAST(GETDATE() AS DATE))) -WHERE M.aktiv = 1 -GROUP BY M.MitarbeiterID, M.Name, M.Email -HAVING COALESCE(SUM(V.AnzahlStriche), 0) < 10 -ORDER BY M.Name; -"; -$stmtMitglieder = sqlsrv_query($conn, $sqlMitglieder); - -// Tabelle erstellen -$html = ' - - - - - - - - - - '; -$y=1; -while ($row = sqlsrv_fetch_array($stmtMitglieder, SQLSRV_FETCH_ASSOC)) { - $mitarbeiterID = $row['MitarbeiterID']; - $name = $row['Name']; - $email = $row['Email']; - $y++; - $gesamteinzahlungen = berechneGesamteinzahlungen($mitarbeiterID, $conn); - $gesamtausgaben = berechneGesamtausgabe($mitarbeiterID, $conn); - $differenztemp = $gesamteinzahlungen - $gesamtausgaben; - $differenz = number_format($differenztemp, 2, ',', '.'); - $html .= ""; - $html .= ""; - #$pdf->writeHTML($html, true, false, true, false, ''); - #$html = ""; - if($differenztemp < -10.00){ - $html .= ''; - }elseif($differenztemp > 5.00){ - $html .= ''; - }else{ - $html .= ""; - } - $html .= ""; - $html .= ""; -} - -for ($i = $y; $i < 65; $i++) { - $html .= ''; -} -$html .= ''; -$html .= '
Kaffeeliste - Wenigtrinker1 Strich = ' . $KostenproStrich . '€. Bitte bezahlen bei 10 € zahlen. ' . date("d.m.Y H:s") . '
NameGuthabenStriche
{$name}' .$differenz . ' €' .$differenz . ' €{$differenz} €
   
  Vorderseite beachten!
'; - - - -#echo $html; -// Tabelle ins PDF einfügen -$pdf->writeHTML($html, true, false, true, false, ''); - -// PDF-Ausgabe -$pdf->Output('Kaffeestrichliste.pdf', 'D'); - - -?> += DATEADD(DAY, -100, (SELECT MAX(Datum) FROM kl_Kaffeeverbrauch WHERE Datum < CAST(GETDATE() AS DATE))) AND M.aktiv = 1 +GROUP BY M.MitarbeiterID, M.Name, M.Email +HAVING SUM(V.AnzahlStriche) >= 10 +ORDER BY Name;"; +$stmtMitglieder = sqlsrv_query($conn, $sqlMitglieder); + +// Kosten pro Strich auslesen +$sqlKostenproStrich = "SELECT KostenproStrich FROM kl_config "; +$stmtKostenproStrich = sqlsrv_query($conn, $sqlKostenproStrich); +$row = sqlsrv_fetch_array($stmtKostenproStrich, SQLSRV_FETCH_ASSOC); +$KostenproStrichtemp = $row["KostenproStrich"]; +$KostenproStrich = number_format($KostenproStrichtemp, 2, ',', '.'); + +// TCPDF-Bibliothek einbinden +require_once('tcpdf/tcpdf.php'); + +class MyCustomPDFWithWatermark extends TCPDF { + public function Header() { + // Get the current page break margin + $bMargin = $this->getBreakMargin(); + + // Get current auto-page-break mode + $auto_page_break = $this->AutoPageBreak; + + // Disable auto-page-break + $this->SetAutoPageBreak(false, 0); + + // Define the path to the image that you want to use as watermark. + $img_file = './watermark.jpg'; + $this->SetAlpha(0.35); + // Render the image + $this->Image($img_file, 0, 0, 223, 280, '', '', '', false, 300, '', false, false, 0); + $this->SetAlpha(1); + // Restore the auto-page-break status + $this->SetAutoPageBreak($auto_page_break, $bMargin); + + // Set the starting point for the page content + $this->setPageMark(); + } +} + + + +// PDF-Objekt erstellen +#$pdf = new TCPDF(); +$pdf = new MyCustomPDFWithWatermark(PDF_PAGE_ORIENTATION, 'mm', 'A4', true, 'UTF-8', false); + + +// PDF-Header setzen +$pdf->SetHeaderData("", 0, "Kaffeestrichliste", ""); + +// PDF-Header und Footer auf jeder Seite anzeigen +#$pdf->SetPrintHeader(false); +$pdf->SetPrintFooter(false); +$pdf->SetMargins('5', '5', '5'); +$pdf->SetAutoPageBreak(TRUE, 5); +// Seitenformat setzen +#$pdf->SetFormat('A4', 'portrait'); +$pdf->SetFont('helvetica', '', 9.5); +// PDF-Inhalt starten +$pdf->AddPage(); + + + +// Tabelle erstellen +$html = ' + + + + + + + + + + '; +$y=1; +while ($row = sqlsrv_fetch_array($stmtMitglieder, SQLSRV_FETCH_ASSOC)) { + $mitarbeiterID = $row['MitarbeiterID']; + $name = $row['Name']; + $email = $row['Email']; + $y++; + $gesamteinzahlungen = berechneGesamteinzahlungen($mitarbeiterID, $conn); + $gesamtausgaben = berechneGesamtausgabe($mitarbeiterID, $conn); + $differenztemp = $gesamteinzahlungen - $gesamtausgaben; + $differenz = number_format($differenztemp, 2, ',', '.'); + $html .= ''; + $html .= ""; + #$pdf->writeHTML($html, true, false, true, false, ''); + #$html = ""; + if($differenztemp < -10.00){ + $html .= ''; + }elseif($differenztemp > 5.00){ + $html .= ''; + }else{ + $html .= ""; + } + $html .= ""; + $html .= ""; +} + +for ($i = $y; $i < 64; $i++) { + $html .= ''; +} +$html .= ''; +$html .= '
Kaffeeliste - Vieltrinker1 Strich = ' . $KostenproStrich . '€. Bitte bezahlen bei 10 € zahlen. ' . date("d.m.Y H:s") . '
NameGuthabenStriche
{$name}' .$differenz . ' €' .$differenz . ' €{$differenz} €
   
  Rückseite beachten!
'; + + +#echo $html; +// Tabelle ins PDF einfügen +$pdf->writeHTML($html, true, false, true, false, ''); + + +// PDF-Inhalt starten +$pdf->AddPage(); + +// Mitglieder aus der Datenbank abrufen +$sqlMitglieder = " +SELECT M.MitarbeiterID, M.Name, M.Email +FROM kl_Mitarbeiter M +LEFT JOIN kl_Kaffeeverbrauch V ON M.MitarbeiterID = V.MitarbeiterID AND V.Datum >= DATEADD(DAY, -100, (SELECT MAX(Datum) FROM kl_Kaffeeverbrauch WHERE Datum < CAST(GETDATE() AS DATE))) +WHERE M.aktiv = 1 +GROUP BY M.MitarbeiterID, M.Name, M.Email +HAVING COALESCE(SUM(V.AnzahlStriche), 0) < 10 +ORDER BY M.Name; +"; +$stmtMitglieder = sqlsrv_query($conn, $sqlMitglieder); + +// Tabelle erstellen +$html = ' + + + + + + + + + + '; +$y=1; +while ($row = sqlsrv_fetch_array($stmtMitglieder, SQLSRV_FETCH_ASSOC)) { + $mitarbeiterID = $row['MitarbeiterID']; + $name = $row['Name']; + $email = $row['Email']; + $y++; + $gesamteinzahlungen = berechneGesamteinzahlungen($mitarbeiterID, $conn); + $gesamtausgaben = berechneGesamtausgabe($mitarbeiterID, $conn); + $differenztemp = $gesamteinzahlungen - $gesamtausgaben; + $differenz = number_format($differenztemp, 2, ',', '.'); + $html .= ""; + $html .= ""; + #$pdf->writeHTML($html, true, false, true, false, ''); + #$html = ""; + if($differenztemp < -10.00){ + $html .= ''; + }elseif($differenztemp > 5.00){ + $html .= ''; + }else{ + $html .= ""; + } + $html .= ""; + $html .= ""; +} + +for ($i = $y; $i < 65; $i++) { + $html .= ''; +} +$html .= ''; +$html .= '
Kaffeeliste - Wenigtrinker1 Strich = ' . $KostenproStrich . '€. Bitte bezahlen bei 10 € zahlen. ' . date("d.m.Y H:s") . '
NameGuthabenStriche
{$name}' .$differenz . ' €' .$differenz . ' €{$differenz} €
   
  Vorderseite beachten!
'; + + + +#echo $html; +// Tabelle ins PDF einfügen +$pdf->writeHTML($html, true, false, true, false, ''); + +// PDF-Ausgabe +$pdf->Output('Kaffeestrichliste.pdf', 'D'); + + +?> diff --git a/faq.php b/legacy-app/faq.php similarity index 98% rename from faq.php rename to legacy-app/faq.php index b5f3495..dec59fb 100644 --- a/faq.php +++ b/legacy-app/faq.php @@ -1,90 +1,90 @@ - - - - - - - - + + + + + + + \ No newline at end of file diff --git a/favicon.ico b/legacy-app/favicon.ico similarity index 100% rename from favicon.ico rename to legacy-app/favicon.ico diff --git a/footer.php b/legacy-app/footer.php similarity index 95% rename from footer.php rename to legacy-app/footer.php index 958090a..8eaef3c 100644 --- a/footer.php +++ b/legacy-app/footer.php @@ -1,61 +1,61 @@ -
-
- - - - - - - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/functions.php b/legacy-app/functions.php similarity index 96% rename from functions.php rename to legacy-app/functions.php index 317f792..98d97ff 100644 --- a/functions.php +++ b/legacy-app/functions.php @@ -1,69 +1,69 @@ -"buffered")); - $row_count = sqlsrv_num_rows( $stmtMitarbeiter ); - #return $row_count; - if ($row_count == 1) - { - return true; - }else{ - return false; - } -} - -function checkKaffeelisteAdmin($conn, $mail) -{ - // Mitarbeiter aus der Datenbank abrufen und nach Namen sortieren - $sqlMitarbeiter = "SELECT MitarbeiterID FROM kl_Mitarbeiter WHERE Email like '" . strtolower($mail) . "' AND admin='1'"; - - $stmtMitarbeiter = sqlsrv_query($conn, $sqlMitarbeiter, array(), array("Scrollable"=>"buffered")); - $row_count = sqlsrv_num_rows( $stmtMitarbeiter ); - #return $row_count; - if ($row_count == 1) - { - return true; - }else{ - return false; - } -} - - +"buffered")); + $row_count = sqlsrv_num_rows( $stmtMitarbeiter ); + #return $row_count; + if ($row_count == 1) + { + return true; + }else{ + return false; + } +} + +function checkKaffeelisteAdmin($conn, $mail) +{ + // Mitarbeiter aus der Datenbank abrufen und nach Namen sortieren + $sqlMitarbeiter = "SELECT MitarbeiterID FROM kl_Mitarbeiter WHERE Email like '" . strtolower($mail) . "' AND admin='1'"; + + $stmtMitarbeiter = sqlsrv_query($conn, $sqlMitarbeiter, array(), array("Scrollable"=>"buffered")); + $row_count = sqlsrv_num_rows( $stmtMitarbeiter ); + #return $row_count; + if ($row_count == 1) + { + return true; + }else{ + return false; + } +} + + ?> \ No newline at end of file diff --git a/functionsLDAP.php b/legacy-app/functionsLDAP.php similarity index 95% rename from functionsLDAP.php rename to legacy-app/functionsLDAP.php index e4df15e..32b1771 100644 --- a/functionsLDAP.php +++ b/legacy-app/functionsLDAP.php @@ -1,166 +1,166 @@ - 0) - { - return $entries[0]['dn']; - } - - return ''; -} - -function getADMail($ad, $samaccountname, $basedn) -{ - $attributes = array('mail'); - $resultz = ldap_search($ad, $basedn, "(samaccountname={$samaccountname})", $attributes); - $entriesz = ldap_get_entries($ad, $resultz); - - #return $entriesz[0]['mail']; - # $entries = ldap_get_entries($ad, $result); - if ($entriesz['count'] > 0) - { - return $entriesz[0]['mail'][0]; - } - - return 'nichts gefunden'; -} - - -/** - * This function retrieves and returns Common Name from a given Distinguished - * Name. - * - * @param string $dn - * The Distinguished Name. - * @return string The Common Name. - */ -function getCN($dn) -{ - preg_match('/[^,]*/', $dn, $matchs, PREG_OFFSET_CAPTURE, 3); - return $matchs[0][0]; -} - -/** - * This function checks group membership of the user, searching only in - * specified group (not recursively). - * - * @param resource $ad - * An LDAP link identifier, returned by ldap_connect(). - * @param string $userdn - * The user Distinguished Name. - * @param string $groupdn - * The group Distinguished Name. - * @return boolean Return true if user is a member of group, and false if not - * a member. - */ -function checkGroup($ad, $userdn, $groupdn) -{ - $result = ldap_read($ad, $userdn, "(memberof={$groupdn})", array( - 'members' - )); - if (! $result) - { - return false; - } - - $entries = ldap_get_entries($ad, $result); - - return ($entries['count'] > 0); -} - -/** - * This function checks group membership of the user, searching in specified - * group and groups which is its members (recursively). - * - * @param resource $ad - * An LDAP link identifier, returned by ldap_connect(). - * @param string $userdn - * The user Distinguished Name. - * @param string $groupdn - * The group Distinguished Name. - * @return boolean Return true if user is a member of group, and false if not - * a member. - */ -function checkGroupEx($ad, $userdn, $groupdn) -{ - if ($groupdn == "") - { - return false; - } - - $result = ldap_read($ad, $userdn, '(objectclass=*)', array( - 'memberof' - )); - if (! $result) - { - return false; - } - - $entries = ldap_get_entries($ad, $result); - if ($entries['count'] <= 0) - { - return false; - } - - if (empty($entries[0]['memberof'])) - { - return false; - } - - for ($i = 0; $i < $entries[0]['memberof']['count']; $i ++) - { - if ($entries[0]['memberof'][$i] == $groupdn) - { - return true; - } - elseif (checkGroupEx($ad, $entries[0]['memberof'][$i], $groupdn)) - { - return true; - } - } - - return false; -} - + 0) + { + return $entries[0]['dn']; + } + + return ''; +} + +function getADMail($ad, $samaccountname, $basedn) +{ + $attributes = array('mail'); + $resultz = ldap_search($ad, $basedn, "(samaccountname={$samaccountname})", $attributes); + $entriesz = ldap_get_entries($ad, $resultz); + + #return $entriesz[0]['mail']; + # $entries = ldap_get_entries($ad, $result); + if ($entriesz['count'] > 0) + { + return $entriesz[0]['mail'][0]; + } + + return 'nichts gefunden'; +} + + +/** + * This function retrieves and returns Common Name from a given Distinguished + * Name. + * + * @param string $dn + * The Distinguished Name. + * @return string The Common Name. + */ +function getCN($dn) +{ + preg_match('/[^,]*/', $dn, $matchs, PREG_OFFSET_CAPTURE, 3); + return $matchs[0][0]; +} + +/** + * This function checks group membership of the user, searching only in + * specified group (not recursively). + * + * @param resource $ad + * An LDAP link identifier, returned by ldap_connect(). + * @param string $userdn + * The user Distinguished Name. + * @param string $groupdn + * The group Distinguished Name. + * @return boolean Return true if user is a member of group, and false if not + * a member. + */ +function checkGroup($ad, $userdn, $groupdn) +{ + $result = ldap_read($ad, $userdn, "(memberof={$groupdn})", array( + 'members' + )); + if (! $result) + { + return false; + } + + $entries = ldap_get_entries($ad, $result); + + return ($entries['count'] > 0); +} + +/** + * This function checks group membership of the user, searching in specified + * group and groups which is its members (recursively). + * + * @param resource $ad + * An LDAP link identifier, returned by ldap_connect(). + * @param string $userdn + * The user Distinguished Name. + * @param string $groupdn + * The group Distinguished Name. + * @return boolean Return true if user is a member of group, and false if not + * a member. + */ +function checkGroupEx($ad, $userdn, $groupdn) +{ + if ($groupdn == "") + { + return false; + } + + $result = ldap_read($ad, $userdn, '(objectclass=*)', array( + 'memberof' + )); + if (! $result) + { + return false; + } + + $entries = ldap_get_entries($ad, $result); + if ($entries['count'] <= 0) + { + return false; + } + + if (empty($entries[0]['memberof'])) + { + return false; + } + + for ($i = 0; $i < $entries[0]['memberof']['count']; $i ++) + { + if ($entries[0]['memberof'][$i] == $groupdn) + { + return true; + } + elseif (checkGroupEx($ad, $entries[0]['memberof'][$i], $groupdn)) + { + return true; + } + } + + return false; +} + ?> \ No newline at end of file diff --git a/header.php b/legacy-app/header.php similarity index 96% rename from header.php rename to legacy-app/header.php index 4b20ac5..d4bd0cb 100644 --- a/header.php +++ b/legacy-app/header.php @@ -1,38 +1,38 @@ - - - - - Kaffeeliste - - - - - -1"; -// Aktuelle Hinweise abrufen -$sql = "SELECT nachricht FROM kl_hinweise WHERE gueltig_bis >= SYSDATETIME() ORDER BY gueltig_bis ASC"; -$stmt = sqlsrv_query($conn, $sql); - -if ($stmt === false) { - die(print_r(sqlsrv_errors(), true)); -} - -if ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) { - echo "
" - . htmlspecialchars($row['nachricht']) . - "
"; -} - -?> - -
- - -
+ + + + + Kaffeeliste + + + + + +1
"; +// Aktuelle Hinweise abrufen +$sql = "SELECT nachricht FROM kl_hinweise WHERE gueltig_bis >= SYSDATETIME() ORDER BY gueltig_bis ASC"; +$stmt = sqlsrv_query($conn, $sql); + +if ($stmt === false) { + die(print_r(sqlsrv_errors(), true)); +} + +if ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) { + echo "
" + . htmlspecialchars($row['nachricht']) . + "
"; +} + +?> + +
+ + +
\ No newline at end of file diff --git a/headerline.php b/legacy-app/headerline.php similarity index 100% rename from headerline.php rename to legacy-app/headerline.php diff --git a/hinweise.php b/legacy-app/hinweise.php similarity index 95% rename from hinweise.php rename to legacy-app/hinweise.php index 8b4b4a5..847f1e4 100644 --- a/hinweise.php +++ b/legacy-app/hinweise.php @@ -1,95 +1,95 @@ - - - - - - - - + + + + + + + \ No newline at end of file diff --git a/index.php b/legacy-app/index.php similarity index 97% rename from index.php rename to legacy-app/index.php index e643852..95810f7 100644 --- a/index.php +++ b/legacy-app/index.php @@ -1,241 +1,241 @@ - - - - - - - - + + + + + + + \ No newline at end of file diff --git a/jahresauswertung.php b/legacy-app/jahresauswertung.php similarity index 96% rename from jahresauswertung.php rename to legacy-app/jahresauswertung.php index 1a98f71..b21c218 100644 --- a/jahresauswertung.php +++ b/legacy-app/jahresauswertung.php @@ -1,177 +1,177 @@ - "Automatisierungsclient", - "Uid" => "ac_admin", - "PWD" => "allacc3ssPW", - "TrustServerCertificate"=>true -); - -$stricheAnzupassen = 490; // Anzahl der neuen Striche -$betragProStrich = 0.20; // Betrag pro Strich in Euro - -// Testmodus aktivieren -$testmodus = false; - - - -use PHPMailer\PHPMailer\PHPMailer; -use PHPMailer\PHPMailer\SMTP; -use PHPMailer\PHPMailer\Exception; - -require 'PHPMailer/src/Exception.php'; -require 'PHPMailer/src/PHPMailer.php'; -require 'PHPMailer/src/SMTP.php'; - -// PHPMailer konfigurieren -function sendeMail($empfaenger, $betreff, $inhalt, $testmodus) -{ - $mail = new PHPMailer(true); - - try { - // Server-Einstellungen - $mail->isSMTP(); - $mail->Host = 'smtpv.aoknds.aok'; // SMTP-Server - $mail->Timeout = 180; - $mail->SMTPAuth = false; - $mail->Port = 25; - - // Absender - $mail->setFrom('kaffeelistesb3@nds.aok.de', 'Kaffeeliste'); - - // Empfänger - if ($testmodus) { - $mail->addAddress('kaffeelistesb3@nds.aok.de'); // Testadresse - } else { - $mail->addAddress($empfaenger); // Tatsächlicher Empfänger - } - - // Inhalt - $mail->isHTML(true); - $mail->Subject = $betreff; - $mail->Body = utf8_decode($inhalt); - - // Senden - $mail->send(); - echo "E-Mail erfolgreich gesendet an: " . ($testmodus ? 'kaffeelistesb3@nds.aok.de' : $empfaenger) . "\n"; - } catch (Exception $e) { - echo "E-Mail konnte nicht gesendet werden. Fehler: {$mail->ErrorInfo}\n"; - } -} - -// Verbindung herstellen -$conn = sqlsrv_connect($serverName, $connectionOptions); -if ($conn === false) { - die(print_r(sqlsrv_errors(), true)); -} - -// Aktuelles Jahr ermitteln -$currentYear = date("Y"); - -// SQL-Abfrage: Gesamtanzahl der Striche pro Mitarbeiter im aktuellen Jahr mit Namen und E-Mail -$sql = " - SELECT - m.MitarbeiterID, - m.Name, - m.Email, - SUM(v.AnzahlStriche) AS GesamtStriche - FROM kl_Kaffeeverbrauch v - JOIN kl_Mitarbeiter m ON v.MitarbeiterID = m.MitarbeiterID - WHERE YEAR(v.Datum) = ? AND m.aktiv = 1 - GROUP BY m.MitarbeiterID, m.Name, m.Email -"; -$params = [$currentYear]; -$stmt = sqlsrv_query($conn, $sql, $params); - -if ($stmt === false) { - die(print_r(sqlsrv_errors(), true)); -} - -// Ergebnisse verarbeiten -$mitarbeiterDaten = []; -$gesamtStriche = 0; - -while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) { - $mitarbeiterDaten[] = $row; - $gesamtStriche += $row['GesamtStriche']; -} - -echo $gesamtStriche; - -// Neue Striche proportional verteilen -$verteilung = []; - -foreach ($mitarbeiterDaten as $mitarbeiter) { - $mitarbeiterID = $mitarbeiter['MitarbeiterID']; - $anteil = $mitarbeiter['GesamtStriche'] / $gesamtStriche; - $neueStriche = round($anteil * $stricheAnzupassen); - $betrag = $neueStriche * $betragProStrich; - - $verteilung[] = [ - 'MitarbeiterID' => $mitarbeiterID, - 'Name' => $mitarbeiter['Name'], - 'Email' => $mitarbeiter['Email'], - 'JahrStriche' => $mitarbeiter['GesamtStriche'], - 'NeueStriche' => $neueStriche, - 'Betrag' => $betrag - ]; - - // SQL-Befehl vorbereiten - $insertSql = " - INSERT INTO kl_Einzahlungen (MitarbeiterID, Betrag, Datum) - VALUES (?, ?, GETDATE()) - "; - $insertParams = [$mitarbeiterID, $betrag]; - - if ($testmodus) { - // SQL-Befehl und Parameter anzeigen - echo "SQL-Befehl: $insertSql\n"; - echo "Parameter: " . json_encode($insertParams) . "\n"; - // E-Mail vorbereiten - - } else { - // SQL-Befehl ausführen - $insertStmt = sqlsrv_query($conn, $insertSql, $insertParams); - if ($insertStmt === false) { - die(print_r(sqlsrv_errors(), true)); - } - // E-Mail vorbereiten - - } - - - - $betreff = "Kaffeeliste - Weihnachten"; - $inhalt = " -

Hallo {$mitarbeiter['Name']},

-

vielen Dank für deine Nutzung der Kaffeeliste in diesem Jahr!

-

Du hast dieses Jahr {$mitarbeiter['GesamtStriche']} Kaffee bezogen.

-

Deswegen wurden dir $betrag Euro auf deinem Konto gutgeschrieben.

-

-

Wir wünschen dir eine frohe Weihnachtszeit und einen guten Rutsch ins neue Jahr.

-

Deine ARGE Kaffeeliste

- "; - $empfaenger = $mitarbeiter['Email']; - // E-Mail senden - sendeMail($empfaenger, $betreff, $inhalt, $testmodus); - - -} - - - -// Sortiere die Verteilung nach JahrStriche (absteigend) -usort($verteilung, function ($a, $b) { - return $b['JahrStriche'] <=> $a['JahrStriche']; -}); - -// Ergebnisse ausgeben -header('Content-Type: application/json'); -#echo json_encode($verteilung, JSON_PRETTY_PRINT); - -// Verbindung schließen -sqlsrv_close($conn); -?> - + "Automatisierungsclient", + "Uid" => "ac_admin", + "PWD" => "allacc3ssPW", + "TrustServerCertificate"=>true +); + +$stricheAnzupassen = 490; // Anzahl der neuen Striche +$betragProStrich = 0.20; // Betrag pro Strich in Euro + +// Testmodus aktivieren +$testmodus = false; + + + +use PHPMailer\PHPMailer\PHPMailer; +use PHPMailer\PHPMailer\SMTP; +use PHPMailer\PHPMailer\Exception; + +require 'PHPMailer/src/Exception.php'; +require 'PHPMailer/src/PHPMailer.php'; +require 'PHPMailer/src/SMTP.php'; + +// PHPMailer konfigurieren +function sendeMail($empfaenger, $betreff, $inhalt, $testmodus) +{ + $mail = new PHPMailer(true); + + try { + // Server-Einstellungen + $mail->isSMTP(); + $mail->Host = 'smtpv.aoknds.aok'; // SMTP-Server + $mail->Timeout = 180; + $mail->SMTPAuth = false; + $mail->Port = 25; + + // Absender + $mail->setFrom('kaffeelistesb3@nds.aok.de', 'Kaffeeliste'); + + // Empfänger + if ($testmodus) { + $mail->addAddress('kaffeelistesb3@nds.aok.de'); // Testadresse + } else { + $mail->addAddress($empfaenger); // Tatsächlicher Empfänger + } + + // Inhalt + $mail->isHTML(true); + $mail->Subject = $betreff; + $mail->Body = utf8_decode($inhalt); + + // Senden + $mail->send(); + echo "E-Mail erfolgreich gesendet an: " . ($testmodus ? 'kaffeelistesb3@nds.aok.de' : $empfaenger) . "\n"; + } catch (Exception $e) { + echo "E-Mail konnte nicht gesendet werden. Fehler: {$mail->ErrorInfo}\n"; + } +} + +// Verbindung herstellen +$conn = sqlsrv_connect($serverName, $connectionOptions); +if ($conn === false) { + die(print_r(sqlsrv_errors(), true)); +} + +// Aktuelles Jahr ermitteln +$currentYear = date("Y"); + +// SQL-Abfrage: Gesamtanzahl der Striche pro Mitarbeiter im aktuellen Jahr mit Namen und E-Mail +$sql = " + SELECT + m.MitarbeiterID, + m.Name, + m.Email, + SUM(v.AnzahlStriche) AS GesamtStriche + FROM kl_Kaffeeverbrauch v + JOIN kl_Mitarbeiter m ON v.MitarbeiterID = m.MitarbeiterID + WHERE YEAR(v.Datum) = ? AND m.aktiv = 1 + GROUP BY m.MitarbeiterID, m.Name, m.Email +"; +$params = [$currentYear]; +$stmt = sqlsrv_query($conn, $sql, $params); + +if ($stmt === false) { + die(print_r(sqlsrv_errors(), true)); +} + +// Ergebnisse verarbeiten +$mitarbeiterDaten = []; +$gesamtStriche = 0; + +while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) { + $mitarbeiterDaten[] = $row; + $gesamtStriche += $row['GesamtStriche']; +} + +echo $gesamtStriche; + +// Neue Striche proportional verteilen +$verteilung = []; + +foreach ($mitarbeiterDaten as $mitarbeiter) { + $mitarbeiterID = $mitarbeiter['MitarbeiterID']; + $anteil = $mitarbeiter['GesamtStriche'] / $gesamtStriche; + $neueStriche = round($anteil * $stricheAnzupassen); + $betrag = $neueStriche * $betragProStrich; + + $verteilung[] = [ + 'MitarbeiterID' => $mitarbeiterID, + 'Name' => $mitarbeiter['Name'], + 'Email' => $mitarbeiter['Email'], + 'JahrStriche' => $mitarbeiter['GesamtStriche'], + 'NeueStriche' => $neueStriche, + 'Betrag' => $betrag + ]; + + // SQL-Befehl vorbereiten + $insertSql = " + INSERT INTO kl_Einzahlungen (MitarbeiterID, Betrag, Datum) + VALUES (?, ?, GETDATE()) + "; + $insertParams = [$mitarbeiterID, $betrag]; + + if ($testmodus) { + // SQL-Befehl und Parameter anzeigen + echo "SQL-Befehl: $insertSql\n"; + echo "Parameter: " . json_encode($insertParams) . "\n"; + // E-Mail vorbereiten + + } else { + // SQL-Befehl ausführen + $insertStmt = sqlsrv_query($conn, $insertSql, $insertParams); + if ($insertStmt === false) { + die(print_r(sqlsrv_errors(), true)); + } + // E-Mail vorbereiten + + } + + + + $betreff = "Kaffeeliste - Weihnachten"; + $inhalt = " +

Hallo {$mitarbeiter['Name']},

+

vielen Dank für deine Nutzung der Kaffeeliste in diesem Jahr!

+

Du hast dieses Jahr {$mitarbeiter['GesamtStriche']} Kaffee bezogen.

+

Deswegen wurden dir $betrag Euro auf deinem Konto gutgeschrieben.

+

+

Wir wünschen dir eine frohe Weihnachtszeit und einen guten Rutsch ins neue Jahr.

+

Deine ARGE Kaffeeliste

+ "; + $empfaenger = $mitarbeiter['Email']; + // E-Mail senden + sendeMail($empfaenger, $betreff, $inhalt, $testmodus); + + +} + + + +// Sortiere die Verteilung nach JahrStriche (absteigend) +usort($verteilung, function ($a, $b) { + return $b['JahrStriche'] <=> $a['JahrStriche']; +}); + +// Ergebnisse ausgeben +header('Content-Type: application/json'); +#echo json_encode($verteilung, JSON_PRETTY_PRINT); + +// Verbindung schließen +sqlsrv_close($conn); +?> + diff --git a/js/dashboard.css b/legacy-app/js/dashboard.css similarity index 100% rename from js/dashboard.css rename to legacy-app/js/dashboard.css diff --git a/js/dashboard.js b/legacy-app/js/dashboard.js similarity index 100% rename from js/dashboard.js rename to legacy-app/js/dashboard.js diff --git a/js/dashboard.rtl.css b/legacy-app/js/dashboard.rtl.css similarity index 100% rename from js/dashboard.rtl.css rename to legacy-app/js/dashboard.rtl.css diff --git a/kaffeeliste.php b/legacy-app/kaffeeliste.php similarity index 96% rename from kaffeeliste.php rename to legacy-app/kaffeeliste.php index 0b30fe0..3eac2c6 100644 --- a/kaffeeliste.php +++ b/legacy-app/kaffeeliste.php @@ -1,148 +1,148 @@ - - - - - + + + + + \ No newline at end of file diff --git a/letzteneintraege.php b/legacy-app/letzteneintraege.php similarity index 96% rename from letzteneintraege.php rename to legacy-app/letzteneintraege.php index 01c0200..f75b27a 100644 --- a/letzteneintraege.php +++ b/legacy-app/letzteneintraege.php @@ -1,238 +1,238 @@ - - - - - + + + + + \ No newline at end of file diff --git a/mailausgebe.php b/legacy-app/mailausgebe.php similarity index 94% rename from mailausgebe.php rename to legacy-app/mailausgebe.php index ab78a5d..b932717 100644 --- a/mailausgebe.php +++ b/legacy-app/mailausgebe.php @@ -1,50 +1,50 @@ - - - - - - - - + + + + + + + \ No newline at end of file diff --git a/mailversenden.php b/legacy-app/mailversenden.php similarity index 96% rename from mailversenden.php rename to legacy-app/mailversenden.php index 6177e54..3f2fad2 100644 --- a/mailversenden.php +++ b/legacy-app/mailversenden.php @@ -1,142 +1,142 @@ - - - - - - + + + + + + \ No newline at end of file diff --git a/mitarbeiterverwalten.php b/legacy-app/mitarbeiterverwalten.php similarity index 96% rename from mitarbeiterverwalten.php rename to legacy-app/mitarbeiterverwalten.php index d29837e..c2121b1 100644 --- a/mitarbeiterverwalten.php +++ b/legacy-app/mitarbeiterverwalten.php @@ -1,262 +1,262 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/namenanpassen.php b/legacy-app/namenanpassen.php similarity index 96% rename from namenanpassen.php rename to legacy-app/namenanpassen.php index 7f928b4..280f636 100644 --- a/namenanpassen.php +++ b/legacy-app/namenanpassen.php @@ -1,122 +1,122 @@ - - - - - -
-
- - - - - - - Anzeigenamen aktualisieren - - - -

Anzeigenamen aktualisieren

- -
Hier kannst du deinen Anzeigenamen anpassen.
Dieser wird auf der Kaffeeliste und E-Mail genutzt.
"; - // SQL-Abfrage für alle Mitarbeiter - $sqlMitarbeiter = "SELECT MitarbeiterID, Name FROM kl_Mitarbeiter WHERE Email ='" . $mailadress . "'"; - $stmtMitarbeiter = sqlsrv_query($conn, $sqlMitarbeiter); - - - } - -// Funktion zum Aktualisieren des Anzeigenamens -function aktualisiereAnzeigenamen($mitarbeiterID, $neuerName, $conn) { - try { - $sql = "UPDATE kl_Mitarbeiter SET Name = ? WHERE MitarbeiterID = ?"; - $params = array($neuerName, $mitarbeiterID); - - $stmt = sqlsrv_query($conn, $sql, $params); - - if ($stmt === false) { - throw new Exception(print_r(sqlsrv_errors(), true)); - } - - return true; // Erfolgreich aktualisiert - } catch (Exception $e) { - return $e->getMessage(); // Fehlermeldung zurückgeben - } -} - -// Überprüfen, ob das Formular abgesendet wurde -if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST["aktion"]) && $_POST["aktion"] == "aktualisieren") { - $mitarbeiterID = $_POST["mitarbeiterID"]; - $neuerName = $_POST["neuerName"]; - - $ergebnis = aktualisiereAnzeigenamen($mitarbeiterID, $neuerName, $conn); - - if ($ergebnis === true) { - echo "Anzeigename erfolgreich aktualisiert."; - } else { - echo "Fehler: $ergebnis"; - } -} - - -?> - - -
"> -
- -
- -
- -

- - - -
- - - - - - -
- -
- -
- -
+ + + + + +
+
+ + + + + + + Anzeigenamen aktualisieren + + + +

Anzeigenamen aktualisieren

+ +
Hier kannst du deinen Anzeigenamen anpassen.
Dieser wird auf der Kaffeeliste und E-Mail genutzt.
"; + // SQL-Abfrage für alle Mitarbeiter + $sqlMitarbeiter = "SELECT MitarbeiterID, Name FROM kl_Mitarbeiter WHERE Email ='" . $mailadress . "'"; + $stmtMitarbeiter = sqlsrv_query($conn, $sqlMitarbeiter); + + + } + +// Funktion zum Aktualisieren des Anzeigenamens +function aktualisiereAnzeigenamen($mitarbeiterID, $neuerName, $conn) { + try { + $sql = "UPDATE kl_Mitarbeiter SET Name = ? WHERE MitarbeiterID = ?"; + $params = array($neuerName, $mitarbeiterID); + + $stmt = sqlsrv_query($conn, $sql, $params); + + if ($stmt === false) { + throw new Exception(print_r(sqlsrv_errors(), true)); + } + + return true; // Erfolgreich aktualisiert + } catch (Exception $e) { + return $e->getMessage(); // Fehlermeldung zurückgeben + } +} + +// Überprüfen, ob das Formular abgesendet wurde +if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST["aktion"]) && $_POST["aktion"] == "aktualisieren") { + $mitarbeiterID = $_POST["mitarbeiterID"]; + $neuerName = $_POST["neuerName"]; + + $ergebnis = aktualisiereAnzeigenamen($mitarbeiterID, $neuerName, $conn); + + if ($ergebnis === true) { + echo "Anzeigename erfolgreich aktualisiert."; + } else { + echo "Fehler: $ergebnis"; + } +} + + +?> + + +
"> +
+ +
+ +
+ +

+ + + +
+ + + + + + + +
+ + + + + \ No newline at end of file diff --git a/nav.php b/legacy-app/nav.php similarity index 75% rename from nav.php rename to legacy-app/nav.php index b1228cf..6c72fa5 100644 --- a/nav.php +++ b/legacy-app/nav.php @@ -1,3 +1,3 @@ - - - + + + diff --git a/stricheintragen.php b/legacy-app/stricheintragen.php similarity index 96% rename from stricheintragen.php rename to legacy-app/stricheintragen.php index 1fd7f38..cdcd4e4 100644 --- a/stricheintragen.php +++ b/legacy-app/stricheintragen.php @@ -1,161 +1,161 @@ - - - - - - - + + + + + + + \ No newline at end of file diff --git a/teilnehmerauswertung.php b/legacy-app/teilnehmerauswertung.php similarity index 97% rename from teilnehmerauswertung.php rename to legacy-app/teilnehmerauswertung.php index 26309bb..c89f5ab 100644 --- a/teilnehmerauswertung.php +++ b/legacy-app/teilnehmerauswertung.php @@ -1,213 +1,213 @@ - - - - - - - - + + + + + + + \ No newline at end of file diff --git a/umfrage.php b/legacy-app/umfrage.php similarity index 97% rename from umfrage.php rename to legacy-app/umfrage.php index 8f7e99d..17efbb8 100644 --- a/umfrage.php +++ b/legacy-app/umfrage.php @@ -1,342 +1,342 @@ - - - - - - + + + + + \ No newline at end of file diff --git a/umfrageergebnisse.php b/legacy-app/umfrageergebnisse.php similarity index 96% rename from umfrageergebnisse.php rename to legacy-app/umfrageergebnisse.php index 76838d6..7535bc2 100644 --- a/umfrageergebnisse.php +++ b/legacy-app/umfrageergebnisse.php @@ -1,370 +1,370 @@ - - - - - - + + + + + \ No newline at end of file diff --git a/watermark.jpg b/legacy-app/watermark.jpg similarity index 100% rename from watermark.jpg rename to legacy-app/watermark.jpg diff --git a/watermark.png b/legacy-app/watermark.png similarity index 100% rename from watermark.png rename to legacy-app/watermark.png diff --git a/saas-app/README.md b/saas-app/README.md index 7518f25..4b4e183 100644 --- a/saas-app/README.md +++ b/saas-app/README.md @@ -1,26 +1,58 @@ -# SaaS App Foundation +# SaaS App -Dieses Verzeichnis enthaelt das neue, Laravel-nahe Zielprojekt fuer die -mandantenfaehige SaaS-Version. +`saas-app/` ist das Zielprojekt fuer die mandantenfaehige Neuimplementierung +der Kaffeeliste als SaaS. -Aktueller Stand: +## Kurzueberblick -- downloadfreies Foundation-Geruest -- modulare Zielstruktur fuer Webspace-Betrieb -- Tenant-Resolution-Skelett -- Basismigrationen fuer Mandanten, Benutzer und Rollen -- einfache Web-Routen und Blade-Platzhalter +- Mandantenfaehigkeit ueber Host/Subdomain +- Rollen und Benutzerbindung pro Tenant +- Kernfunktionen fuer Dashboard, Mitglieder, Einzahlungen und Striche +- Inhalte, Hinweise, Importe, Exporte und Benachrichtigungen als eigene Module +- SSR-orientierter Betrieb fuer klassischen Webspace +- Cron-basierter Betrieb statt dauerhafter Worker -Zielrahmen: +## Installation -- klassischer Webspace / PHP-Hosting -- SSR-orientiert -- Cron statt dauerhaft laufender Worker -- OIDC zuerst, SAML spaeter optional +Die komplette Installationsanleitung steht im Repo unter +`../docs/installationshandbuch.md`. -Hinweis: +Kurzfassung: -Dieses Geruest ersetzt noch kein vollstaendig installiertes Laravel-Projekt. -Sobald `php` und `composer` verfuegbar sind, soll auf dieser Struktur ein -vollwertiges Laravel-Projekt aufgesetzt oder diese Struktur in ein solches -ueberfuehrt werden. +1. `saas-app/` als Projektrahmen bereitstellen. +2. PHP 8.2+ und Composer verwenden. +3. `.env` aus `.env.example` ableiten und anpassen. +4. Datenbank und Tenancy-Werte konfigurieren. +5. Migrations ausfuehren. +6. Einen ersten Mandanten und erste Benutzer anlegen. +7. Den Webserver auf `public/` ausrichten. +8. Cron-Jobs fuer Queue, Import, Export und Benachrichtigungen einrichten. + +## Migration Aus Dem Legacy-System + +Die fachliche Roadmap und der Uebergang aus dem alten Root-System sind in +`../docs/implementation-foundation.md` beschrieben. + +Der relevante Kern der alten Anwendung besteht im Wesentlichen aus: + +- Dashboard und Kontostand +- Mitgliederverwaltung +- Kaffee-Striche +- Einzahlungen +- Hinweise und Inhalte +- Exporte und operative Hilfsfunktionen + +## Hosting-Hinweise + +- Das Projekt ist fuer Webspace geeignet, solange PHP, DB-Zugang und Cron + vorhanden sind. +- Dauerhafte Queue-Worker sind nicht vorausgesetzt. +- Der zentrale Einstieg erfolgt ueber die mandantenfaehige Weboberflaeche. +- OIDC ist als bevorzugter SSO-Pfad vorgesehen, klassische Logins bleiben als + Fallback moeglich. + +## Aktueller Stand + +Das Verzeichnis ist als Zielarchitektur vorbereitet. Es ersetzt den Legacy-Root +noch nicht vollstaendig, sondern dient als naechster konsistenter Zielzustand +fuer die SaaS-Umstellung. diff --git a/saas-app/bootstrap/app.php b/saas-app/bootstrap/app.php index 5042f41..9685f11 100644 --- a/saas-app/bootstrap/app.php +++ b/saas-app/bootstrap/app.php @@ -13,6 +13,22 @@ declare(strict_types=1); */ return [ - 'status' => 'foundation-skeleton', + 'status' => 'saas-first-blueprint', 'framework' => 'laravel-near', + 'runtime_target' => 'webspace-ssr', + 'modules' => [ + 'dashboard', + 'members', + 'ledger', + 'payments', + 'content', + 'imports', + 'exports', + 'notifications', + 'tenants', + 'identity', + ], + 'optional_modules' => [ + 'surveys', + ], ]; diff --git a/saas-app/config/multitenancy.php b/saas-app/config/multitenancy.php index 5aaacd7..9932bef 100644 --- a/saas-app/config/multitenancy.php +++ b/saas-app/config/multitenancy.php @@ -2,10 +2,8 @@ return [ 'mode' => env('TENANCY_MODE', 'subdomain'), - 'central_domains' => [ - 'app.example.com', - 'www.app.example.com', - ], + 'central_domains' => explode(',', (string) env('TENANCY_CENTRAL_DOMAINS', 'localhost')), 'tenant_parameter' => 'tenant', 'default_route' => 'dashboard', + 'fallback_tenant' => env('TENANCY_FALLBACK_TENANT'), ]; diff --git a/saas-app/public/.htaccess b/saas-app/public/.htaccess new file mode 100644 index 0000000..5bc0437 --- /dev/null +++ b/saas-app/public/.htaccess @@ -0,0 +1,6 @@ + + RewriteEngine On + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule ^ index.php [L] + diff --git a/saas-app/public/README.md b/saas-app/public/README.md new file mode 100644 index 0000000..63f6fd1 --- /dev/null +++ b/saas-app/public/README.md @@ -0,0 +1,9 @@ +Dieses Verzeichnis ist fuer den Webserver-Document-Root vorgesehen. + +Aktuell enthaelt es bereits: + +- `index.php` als Preview-Einstieg fuer die neue SaaS-Struktur +- `.htaccess` fuer einen einfachen Front-Controller-Pfad auf Apache + +Sobald die Zielanwendung als vollwertiges Laravel-Projekt gebootstrapped ist, +zeigt der Webserver weiterhin auf `saas-app/public/`. diff --git a/saas-app/public/index.php b/saas-app/public/index.php new file mode 100644 index 0000000..a58199e --- /dev/null +++ b/saas-app/public/index.php @@ -0,0 +1,234 @@ + 'Dashboard', + 'copy' => 'Kontostand, Monatsverbrauch, letzte Buchungen und Self-Service fuer Kaffee-Striche.', + 'tone' => 'core', + ], + [ + 'title' => 'Members', + 'copy' => 'Mitglieder, Rollen, Aktivstatus und Identitaeten pro Mandant.', + 'tone' => 'core', + ], + [ + 'title' => 'Ledger', + 'copy' => 'Kaffee-Striche, Einzahlungen und Korrekturen in einer nachvollziehbaren Sicht.', + 'tone' => 'core', + ], + [ + 'title' => 'Payments', + 'copy' => 'Sammelerfassung, PayPal-Pfade und spaetere Zahlungsreferenzen.', + 'tone' => 'core', + ], + [ + 'title' => 'Content', + 'copy' => 'Hinweise, FAQ und tenantbezogene Inhalte statt Root-Einzeldateien.', + 'tone' => 'ops', + ], + [ + 'title' => 'Operations', + 'copy' => 'Importe, Exporte und Benachrichtigungen als saubere Backoffice-Module.', + 'tone' => 'ops', + ], +]; + +?> + + + + + Kaffeeliste SaaS Preview + + + +
+
+
Kaffeeliste SaaS Preview
+

Der Root ist jetzt SaaS-first aufgebaut.

+

+ Diese Preview-Seite markiert den neuen Document-Root unter `saas-app/public/`. + Der Legacy-Bestand wurde nach `legacy-app/` verschoben, die Zielarchitektur + und die neu gestalteten Produktseiten liegen unter `saas-app/`. +

+ +
+ Mandantenfaehig + Webspace-tauglich + Legacy archiviert +
+
+ Die eigentliche Runtime bleibt der naechste Schritt nach Composer-Bootstrap. + Layout, Module, Dokumentation und Hosting-Zielbild sind bereits auf die + SaaS-Zielstruktur umgestellt. +
+
+ +
+ +
+

+

+
+ +
+ + +
+ + diff --git a/saas-app/resources/views/auth/forgot-password.blade.php b/saas-app/resources/views/auth/forgot-password.blade.php index 8259f58..0749eee 100644 --- a/saas-app/resources/views/auth/forgot-password.blade.php +++ b/saas-app/resources/views/auth/forgot-password.blade.php @@ -1,14 +1,54 @@ @extends('layouts.app') -@section('content') -
-

Passwort zuruecksetzen

-

Platzhalter fuer den webspace-tauglichen Passwort-Reset-Prozess per E-Mail.

+@section('page_title', 'Kaffeeliste SaaS - Passwort Reset') -
- - - -
+@section('content') +
+
+

Reset flow

+

Ein einfacher Reset-Prozess fuer lokale Zugangswege.

+

+ Wenn ein Mandant keinen SSO-Provider nutzt oder ein lokaler Fallback aktiv + bleibt, fuehrt diese Seite den Passwort-Reset ueber Mailversand und + zeitlich begrenzte Tokens. +

+
+
+ SMTP + Token storage + Webspace fit +
+
+ +
+
+

Reset anstossen

+

Reset-Link anfordern

+
+
+ + +
+ +
+
+ +
+

Betriebshinweise

+
+
+

Tenant-Kontext beachten

+

Der Link gilt nur fuer das aktive Mandantenumfeld.

+
+
+

Mail korrekt konfigurieren

+

SMTP, Absender und Ablaufzeiten muessen in der finalen Runtime gesetzt werden.

+
+
+

Nur fuer lokale Accounts

+

Externe OIDC-Provider behalten ihre eigenen Reset-Wege.

+
+
+
@endsection diff --git a/saas-app/resources/views/auth/login.blade.php b/saas-app/resources/views/auth/login.blade.php index 4202ac4..c035467 100644 --- a/saas-app/resources/views/auth/login.blade.php +++ b/saas-app/resources/views/auth/login.blade.php @@ -1,28 +1,59 @@ @extends('layouts.app') +@section('page_title', 'Kaffeeliste SaaS - Login') + @section('content') -
-

Login

-

Mandantenbezogener Einstieg fuer lokalen Login oder spaetere SSO-Anmeldung.

+
+
+

Identity

+

Mandantenbezogener Login mit lokalem Fallback und OIDC-Zielbild.

+

+ Die Legacy-Anbindung ueber `AUTH_USER` und LDAP wird in eine flexiblere + Identity-Schicht ueberfuehrt. SSO bleibt bevorzugt, lokaler Login und + Reset-Prozess sichern den Betrieb ab. +

+
+
+ Tenant erkannt + OIDC first + Fallback bereit +
+
-
- - +
+
+

Lokaler Login

+

Mit Passwort anmelden

+ +
+ + +
+
+ + +
+
+ + Passwort vergessen? +
+ +
- - - - - - -

Passwort vergessen?

- -
+

Single Sign-on

-

Hier werden spaeter tenantbezogene OIDC-/ADFS-Provider angeboten.

- -
+

+ Tenant-Admins hinterlegen je Mandant einen oder mehrere OIDC-Provider. + So wird die bisherige AD-Naehe in ein flexibles SaaS-Modell ueberfuehrt. +

+
+ Mit OIDC / Entra anmelden + Provider: entra-default + Tenant: demo +
+
+ Lokaler Login bleibt wichtig fuer Initialsetup, Notfallbetrieb und kleinere Tenants ohne SSO. +
+
@endsection diff --git a/saas-app/resources/views/content/index.blade.php b/saas-app/resources/views/content/index.blade.php index a998f62..af03502 100644 --- a/saas-app/resources/views/content/index.blade.php +++ b/saas-app/resources/views/content/index.blade.php @@ -1,8 +1,78 @@ @extends('layouts.app') +@section('page_title', 'Kaffeeliste SaaS - Content') + @section('content') -
-

Hinweise und FAQ

-

Platzhalter fuer tenantbezogene Hinweise, FAQ-Eintraege und spaetere Redaktionsfunktionen.

+
+
+

Content

+

Hinweise, Banner und FAQ werden tenantbezogene Inhalte.

+

+ Die globale Hinweislogik aus der alten Oberflaeche wird zu einem + eigenstaendigen Redaktionsbereich. Banner, FAQ und Hilfetexte sind nicht + mehr im Root versteckt, sondern pro Mandant pflegbar. +

+
+
+ Announcements + FAQ + Tenant scoped +
+
+ +
+
+
+
+

Aktiv

+

Hinweise im Umlauf

+
+ Header + Dashboard +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
TitelSichtbar bisKanalStatus
Monatsabschluss am Freitag31.03.2026DashboardAktiv
Neuer Preis pro Strich15.04.2026HeaderGeplant
+
+
+ +
+

Redaktioneller Nutzen

+
+
+

Hinweise ohne Code-Aenderung

+

Tenant-Admins koennen sichtbare Meldungen direkt verwalten.

+
+
+

FAQ je Organisation

+

Hilfeinhalte koennen pro Mandant und Prozess gepflegt werden.

+
+
+

Saubere Sichtbarkeit

+

Dashboard, Header und spaetere Mailtexte greifen auf denselben Content-Pool zu.

+
+
+
@endsection diff --git a/saas-app/resources/views/dashboard/index.blade.php b/saas-app/resources/views/dashboard/index.blade.php index 3fdbed2..10500e3 100644 --- a/saas-app/resources/views/dashboard/index.blade.php +++ b/saas-app/resources/views/dashboard/index.blade.php @@ -1,13 +1,102 @@ @extends('layouts.app') +@section('page_title', 'Kaffeeliste SaaS - Dashboard') + @section('content') -
-

Dashboard

-

Platzhalter fuer Kontostand, Monatsverbrauch und letzte Buchungen.

-
    -
  • Kontostand: 7,50 EUR
  • -
  • Striche diesen Monat: 5
  • -
  • Einzahlungen diesen Monat: 1
  • -
+
+
+

Tenant overview

+

Dein Kaffeeliste-Stand auf einen Blick.

+

+ Das Dashboard zeigt den aktuellen Kontostand, die Nutzung im Monat und die letzten Buchungen. + Die Werte sind hier bewusst als fachliche Anker platziert und koennen spaeter direkt aus dem Ledger gespeist werden. +

+
+
+ Letzte Synchronisation: heute + Tenant: Demo-Workspace + Saldo aktiv +
+
+ +
+
+

Kontostand

+
7,50 EUR
+

Positiver Puffer fuer den laufenden Monat.

+
+
+

Striche diesen Monat

+
5
+

Verbrauch seit Monatsbeginn.

+
+
+

Einzahlungen diesen Monat

+
1
+

Alle gebuchten Zahlungen im aktuellen Zeitraum.

+
+
+

Letzte Buchung

+
Heute
+

Aktuelle Aktivitaet im Mandantenbereich.

+
+
+ +
+
+
+
+

Aktivitaet

+

Letzte Buchungen

+
+ Live feed +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ZeitTypBetragStatus
09:00Einzahlung10,00 EURGebucht
10:30Strich-2,50 EURVerbraucht
12:15HinweisInfoAktiv
+
+
+ +
@endsection diff --git a/saas-app/resources/views/exports/index.blade.php b/saas-app/resources/views/exports/index.blade.php index 8150ff9..1895848 100644 --- a/saas-app/resources/views/exports/index.blade.php +++ b/saas-app/resources/views/exports/index.blade.php @@ -1,8 +1,75 @@ @extends('layouts.app') +@section('page_title', 'Kaffeeliste SaaS - Exports') + @section('content') -
-

Exporte

-

Platzhalter fuer Reports, Export-Jobs und spaetere Download-Ansichten.

+
+
+

Exports

+

Reports und Drucklisten bleiben moeglich, aber nicht mehr als Spezialskript.

+

+ Das fruehere PDF fuer die Papierliste wandert in ein Export-Modul. Neben + Drucklisten entstehen hier Reports fuer Finance, Mitglieder und + Monatsabschluesse. +

+
+
+ PDF optional + Reports + Digital first +
+
+ +
+
+

Exportziele

+
    +
  • Kontostandsreport Standardausgabe fuer Finance und Monatsabschluss.
  • +
  • Mitgliederexport Snapshot der aktiven und inaktiven Nutzer pro Tenant.
  • +
  • Papierliste Optionaler PDF-Weg fuer Teams mit analogem Prozess.
  • +
+
+ +
+
+
+

Downloads

+

Letzte Exportjobs

+
+ Report history +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ExportZielFormatStatus
Monatsreport MaerzFinanceCSVBereit
Papierliste VieltrinkerKuechePDFBereit
Mitglieder-SnapshotTenant AdminXLSX spaeterGeplant
+
+
@endsection diff --git a/saas-app/resources/views/imports/index.blade.php b/saas-app/resources/views/imports/index.blade.php index d16e79b..a270cbe 100644 --- a/saas-app/resources/views/imports/index.blade.php +++ b/saas-app/resources/views/imports/index.blade.php @@ -1,8 +1,83 @@ @extends('layouts.app') +@section('page_title', 'Kaffeeliste SaaS - Imports') + @section('content') -
-

Importe

-

Platzhalter fuer CSV-Importe, Cron-gesteuerte Verarbeitungen und Statusanzeigen.

+
+
+

Imports

+

Dateiimporte werden kontrollierte Jobs statt einmaliger Root-Skripte.

+

+ CSV-Uploads und Legacy-Datenuebernahmen bleiben moeglich, laufen aber als + nachvollziehbare Importjobs mit Vorschau, Mapping und Statusmeldungen. +

+
+
+ CSV optional + Cron ready + Migration friendly +
+
+ +
+
+

Import-Pipeline

+
+
+

1. Datei pruefen

+

Format, Pflichtspalten und Deduplikation werden vorab validiert.

+
+
+

2. Mapping anwenden

+

Legacy-Zahlungen und Verbrauch werden auf Members, Payments und Ledger abgebildet.

+
+
+

3. Job ausfuehren

+

Verarbeitung laeuft ueber Cron und schreibt einen nachvollziehbaren Status.

+
+
+
+ +
+
+
+

Queue

+

Letzte Importjobs

+
+ Backoffice +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
JobQuelleErgebnisStatus
members-2026-03Legacy Export48 DatensaetzeFertig
payments-2026-03CSV PayPal12 BuchungenVerarbeitet
ledger-replayArchivbestand3 WarnungenPruefen
+
+
@endsection diff --git a/saas-app/resources/views/layouts/app.blade.php b/saas-app/resources/views/layouts/app.blade.php index 3cf0560..cc0442d 100644 --- a/saas-app/resources/views/layouts/app.blade.php +++ b/saas-app/resources/views/layouts/app.blade.php @@ -3,15 +3,370 @@ - {{ $title ?? 'Kaffeeliste SaaS' }} + + @yield('page_title', $title ?? 'Kaffeeliste SaaS') + - -
-

Kaffeeliste SaaS

-

Mandantenfaehige Webspace-Zielanwendung

-
-
- @yield('content') -
+ +
+
+
+
+
K
+
+

Kaffeeliste SaaS

+

Mandantenfaehige Buchungen, Mitglieder und Auswertungen

+
+
+
+ Webspace-ready + Tenant aware +
+
+ +
+ +
+ @yield('content') +
+ +
+ +
+
diff --git a/saas-app/resources/views/ledger/index.blade.php b/saas-app/resources/views/ledger/index.blade.php index 091fad5..fe189a7 100644 --- a/saas-app/resources/views/ledger/index.blade.php +++ b/saas-app/resources/views/ledger/index.blade.php @@ -1,8 +1,81 @@ @extends('layouts.app') +@section('page_title', 'Kaffeeliste SaaS - Ledger') + @section('content') -
-

Ledger

-

Platzhalter fuer Buchungen, Kontostand und Korrekturen.

+
+
+

Accounting flow

+

Ledger fuer Buchungen, Verbrauch und Korrekturen.

+

+ Die fachliche Kernlogik der alten Kaffeeliste bleibt erhalten: Einzahlungen, Striche und Saldo + werden in einer nachvollziehbaren Buchungsspur zusammengefuehrt. +

+
+
+ Saldo: 7,50 EUR + Korrekturen: 2 + Buchungsspur geordnet +
+
+ +
+
+

Offener Saldo

+
7,50 EUR
+

Positiv, daher kein Handlungsdruck.

+
+
+

Verbrauchsumfang

+
5 Striche
+

Aktueller Buchungsumfang im laufenden Zeitraum.

+
+
+

Letzte Korrektur

+
Heute
+

Sichtbar fuer Admins und Audit.

+
+
+ +
+
+
+

Buchungen

+

Ledger-Eintraege

+
+ Audit trail +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DatumBeschreibungTypBetrag
21.03.2026Einzahlung Demo-WorkspacePayment+10,00 EUR
21.03.20262 Striche KaffeeConsumption-2,50 EUR
20.03.2026Manuelle KorrekturAdjustment+0,50 EUR
+
@endsection diff --git a/saas-app/resources/views/members/index.blade.php b/saas-app/resources/views/members/index.blade.php index c6517fa..167ea24 100644 --- a/saas-app/resources/views/members/index.blade.php +++ b/saas-app/resources/views/members/index.blade.php @@ -1,8 +1,76 @@ @extends('layouts.app') +@section('page_title', 'Kaffeeliste SaaS - Mitglieder') + @section('content') -
-

Mitglieder

-

Platzhalter fuer tenantbezogene Mitgliederverwaltung und Statusanzeige.

+
+
+

Member management

+

Mitgliederverwaltung pro Mandant.

+

+ Hier sind aktive Mitglieder, Rollen und Status klar gegliedert. Diese Sicht entspricht der alten + Mitgliederverwaltung, nur als SaaS-taugliche, aufgeraeumte Oberflaeche. +

+
+
+ Aktiv: 18 + Admins: 3 + Tenant: Demo-Workspace +
+
+ +
+
+

Aktive Mitglieder

+
18
+

Fuer Buchungen, Auswertungen und Benachrichtigungen freigeschaltet.

+
+
+

Sperrstatus

+
2
+

Inaktive Konten bleiben sichtbar, aber ohne Schreibrechte.

+
+
+ +
+
+
+

Mitgliederliste

+

Aktive Zuordnungen

+
+ Rollenkonzept +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameE-MailRolleStatus
Max Beispielmax@example.comMemberAktiv
Jana Musterjana@example.comAdminAktiv
Chris Reservechris@example.comMemberPausiert
+
@endsection diff --git a/saas-app/resources/views/notifications/index.blade.php b/saas-app/resources/views/notifications/index.blade.php index a70a5a4..a6bb04c 100644 --- a/saas-app/resources/views/notifications/index.blade.php +++ b/saas-app/resources/views/notifications/index.blade.php @@ -1,8 +1,84 @@ @extends('layouts.app') +@section('page_title', 'Kaffeeliste SaaS - Notifications') + @section('content') -
-

Benachrichtigungen

-

Platzhalter fuer Mailversand, Versandstatus und Cron-basierte Zustellung.

+
+
+

Notifications

+

Benachrichtigungen werden planbare Betriebsprozesse.

+

+ Die alte Sammelmail-Funktion geht in ein Modul ueber, das Versandregeln, + Cron-Ausfuehrung und Ergebnisprotokolle sauber trennt. Damit werden + Schuldenhinweise und Service-Meldungen tenantfaehig. +

+
+
+ Mail templates + Cron dispatch + Audit logs +
+
+ +
+
+

Benachrichtigungsarten

+
+
+

Saldo-Erinnerung

+

Automatisch bei negativem Kontostand oder Schwellwerten.

+
+
+

Import- und Exportstatus

+

Backoffice meldet Erfolge, Warnungen und Fehler an Admins.

+
+
+

Service-Kommunikation

+

Hinweise zu Preisen, Wartung oder Monatsabschluss zentral ausspielen.

+
+
+
+ +
+
+
+

Versandprotokoll

+

Letzte Benachrichtigungen

+
+ Tenant scoped +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypEmpfaengerAusloeserStatus
Saldo-Erinnerung6 Mitgliedernegativer KontostandGesendet
Import-ReportTenant AdminImportjob fertigBereitgestellt
Service-Hinweisalle MitgliederPreisanpassungEingeplant
+
+
@endsection diff --git a/saas-app/resources/views/payments/index.blade.php b/saas-app/resources/views/payments/index.blade.php index c5c2645..f39181c 100644 --- a/saas-app/resources/views/payments/index.blade.php +++ b/saas-app/resources/views/payments/index.blade.php @@ -1,8 +1,96 @@ @extends('layouts.app') +@section('page_title', 'Kaffeeliste SaaS - Payments') + @section('content') -
-

Einzahlungen

-

Platzhalter fuer Zahlungseintraege und spaetere Zahlungsreferenzen.

+
+
+

Payments

+

Einzahlungen und Zahlungswege werden zu einem eigenen SaaS-Modul.

+

+ Die fruehere Sammelerfassung bleibt als Kernfunktion erhalten, wird aber um + Status, Referenzen und tenantbezogene Bezahlwege erweitert. So passen + manuelle Buchung, PayPal und spaetere Automatisierungen in denselben Ablauf. +

+
+
+ PayPal optional + Finance workflow + Saldo-relevant +
+
+ +
+
+

Offene Zahlungen

+
6
+

Noch zu bestaetigende oder abzugleichende Vorgaege.

+
+
+

Monatssumme

+
245 EUR
+

Gebuchte Einzahlungen im aktuellen Abrechnungszeitraum.

+
+
+

Schnellwege

+
3
+

Bar, PayPal und spaetere Referenzimporte aus Drittsystemen.

+
+
+ +
+
+
+
+

Zahlungsjournal

+

Letzte Zahlungseintraege

+
+ Finance Sicht +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MitgliedMethodeBetragStatus
Max BeispielPayPal10,00 EURGebucht
Julia BetriebBar5,00 EURBestaetigt
Rene MusterSEPA Import15,00 EURPruefen
+
+
+ +
+

Was aus dem Legacy-System bleibt

+
    +
  • Sammelerfassung Admins buchen mehrere Einzahlungen in einem Schritt.
  • +
  • Dashboard-Link Direkte Einzahlung oder Schuldenausgleich bleibt moeglich.
  • +
  • Ledger Sync Jede Einzahlung wirkt unmittelbar auf den Kontostand.
  • +
+
+ Das Payments-Modul ersetzt keine Funktionalitaet, sondern gibt ihr eine saubere Prozessstruktur. +
+
@endsection diff --git a/saas-app/resources/views/surveys/index.blade.php b/saas-app/resources/views/surveys/index.blade.php index 6479574..2d95bef 100644 --- a/saas-app/resources/views/surveys/index.blade.php +++ b/saas-app/resources/views/surveys/index.blade.php @@ -1,8 +1,42 @@ @extends('layouts.app') +@section('page_title', 'Kaffeeliste SaaS - Surveys') + @section('content') -
-

Umfragen

-

Platzhalter fuer Umfragen, Fragen, Antworten und spaetere Auswertungen.

+
+
+

Optionales Modul

+

Umfragen bleiben moeglich, sind aber nicht mehr Teil des Pflichtkerns.

+

+ Das Survey-Modul ist weiterhin vorgesehen, wird aber bewusst als optionaler + Baustein gefuehrt. So bleibt der eigentliche Produktkern schlank und die + fruehere Zusatzfunktion geht nicht verloren. +

+
+
+ Feature flag + Tenant scoped + Optional +
+
+ +
+
+

Typische Einsatzfaelle

+
    +
  • Feedback Kurze Stimmungsbilder zu Kaffee, Preisen oder Ausstattung.
  • +
  • Organisation Abstimmungen zu Office-Regeln und Betriebsroutinen.
  • +
+
+
+

Produktentscheidung

+

+ Das Modul ist vorbereitet, blockiert aber weder Migration noch Go-live. Aktiviert wird es nur, + wenn ein Mandant den Bedarf wirklich hat. +

+
+ Surveys bleiben ein Erweiterungsmodul und ueberlagern nicht mehr Dashboard, Ledger oder Payments. +
+
@endsection diff --git a/saas-app/resources/views/tenants/index.blade.php b/saas-app/resources/views/tenants/index.blade.php index 1fad9a4..36122a1 100644 --- a/saas-app/resources/views/tenants/index.blade.php +++ b/saas-app/resources/views/tenants/index.blade.php @@ -1,25 +1,93 @@ @extends('layouts.app') -@section('content') -
-

Tenant-Verwaltung

-

Platzhalter fuer die spaetere Verwaltung von Mandanten, Domains, Rollen und SSO-Providern.

+@section('page_title', 'Kaffeeliste SaaS - Tenants') - - - - - - - - - - - - - - - -
MandantTenant KeyStatus
Demo Tenantdemoactive
+@section('content') +
+
+

Central admin

+

Mandanten, Domains und SSO sauber zentral verwalten.

+

+ Die SaaS-Version fuehrt eine echte Mandantenebene ein. So werden mehrere + Teams, Standorte oder Fachbereiche in derselben Plattform betrieben, aber + fachlich und organisatorisch getrennt. +

+
+
+ Subdomain routing + OIDC first + Tenant aware +
+
+ +
+
+

Mandanten

+
3
+

Aktive Bereiche auf gemeinsamer Plattform.

+
+
+

Domains

+
5
+

Zentrale und tenantbezogene Hosts fuer Login und Betrieb.

+
+
+

Provider

+
2
+

OIDC-Provider plus lokaler Fallback.

+
+
+ +
+
+
+
+

Mandantenlandschaft

+

Aktive Tenants

+
+ Central view +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MandantTenant KeyDomainStatus
Office Berlinberlinberlin.kaffeeliste.appAktiv
Office Koelnkoelnkoeln.kaffeeliste.appAktiv
Shared Demodemodemo.kaffeeliste.appSandbox
+
+
+ +
+

Zentrale Aufgaben

+
    +
  • Domain Setup Host und Tenant Key fuer saubere Aufloesung.
  • +
  • Identity OIDC-Provider pro Mandant hinterlegen.
  • +
  • Feature Flags PayPal, Surveys oder Self-Service-Striche je Tenant steuern.
  • +
+
@endsection diff --git a/saas-app/resources/views/welcome.blade.php b/saas-app/resources/views/welcome.blade.php index cd65635..332e129 100644 --- a/saas-app/resources/views/welcome.blade.php +++ b/saas-app/resources/views/welcome.blade.php @@ -1,8 +1,67 @@ @extends('layouts.app') +@section('page_title', 'Kaffeeliste SaaS - Landing') + @section('content') -
-

Neue SaaS-Plattform

-

Dieses Grundgeruest dient als Startpunkt fuer die mandantenfaehige Neuimplementierung.

+
+
+

Coffee operations cloud

+

Die neue SaaS-Zentrale fuer Kaffeeliste, Mitglieder und Buchungen.

+

+ Diese Version stellt den Produktkern sichtbar in den Vordergrund: Mandanten, Login, Kontostaende, + Striche, Einzahlungen, Hinweise und Auswertungen in einer klaren, webspace-tauglichen Oberflaeche. +

+ +
+ +
+
+

MVP

+

Kernfunktionen bleiben erhalten

+

Mitglieder, Ledger, Einzahlungen, Striche und Hinweise sind fachlich sichtbar modelliert.

+
+
+

SaaS

+

Mandantenfaehig gedacht

+

Jede Organisation bekommt ihren eigenen Bereich, eigene Rollen und eigene Inhalte.

+
+
+

Hosting

+

Webspace und Cron zuerst

+

Die Zielarchitektur bleibt leichtgewichtig und vermeidet dauerhaft laufende Worker.

+
+
+
+ +
+
+

Was die Plattform abdeckt

+
    +
  • Dashboard Kontostand, Monatswerte und letzte Aktionen.
  • +
  • Ledger Einzahlungen, Verbrauch und Korrekturen in einer Sicht.
  • +
  • Members Aktivitaet, Rollen und Mitgliedsstatus pro Mandant.
  • +
  • Operations Importe, Exporte, Notifications und Surveys als Zusatzmodule.
  • +
+
+
+

Projektfokus

+
+
+

1. Root modernisieren

+

Die alte PHP-Oberflaeche wird fachlich in die SaaS-Struktur ueberfuehrt.

+
+
+

2. Nicht benoetigte Seiten entlasten

+

Sonderlogik wandert in optionale Module oder faellt bewusst weg.

+
+
+

3. Doku und Betrieb

+

Installationsanleitung, Hosting-Hinweise und Migrationspfad bleiben nachvollziehbar.

+
+
+
@endsection diff --git a/scripts/check-prerequisites.ps1 b/scripts/check-prerequisites.ps1 new file mode 100644 index 0000000..3606235 --- /dev/null +++ b/scripts/check-prerequisites.ps1 @@ -0,0 +1,48 @@ +param( + [string]$ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path +) + +$ErrorActionPreference = 'Stop' + +$saasAppPath = Join-Path $ProjectRoot 'saas-app' +$envExamplePath = Join-Path $saasAppPath '.env.example' + +function Test-Command { + param([string]$Name) + + return $null -ne (Get-Command $Name -ErrorAction SilentlyContinue) +} + +function Write-Check { + param( + [string]$Label, + [bool]$Passed, + [string]$Detail + ) + + $status = if ($Passed) { '[OK]' } else { '[FEHLT]' } + Write-Host "$status $Label - $Detail" +} + +Write-Host 'Pruefe lokale Voraussetzungen fuer Kaffeeliste SaaS...' +Write-Host "Projektwurzel: $ProjectRoot" +Write-Host '' + +$phpExists = Test-Command 'php' +$composerExists = Test-Command 'composer' +$gitExists = Test-Command 'git' + +Write-Check -Label 'PHP' -Passed $phpExists -Detail ($(if ($phpExists) { (php -r "echo PHP_VERSION;") } else { 'nicht gefunden' })) +Write-Check -Label 'Composer' -Passed $composerExists -Detail ($(if ($composerExists) { (composer --version | Select-Object -First 1) } else { 'nicht gefunden' })) +Write-Check -Label 'Git' -Passed $gitExists -Detail ($(if ($gitExists) { 'verfuegbar' } else { 'nicht gefunden' })) +Write-Check -Label 'SaaS-App' -Passed (Test-Path $saasAppPath) -Detail $saasAppPath +Write-Check -Label '.env.example' -Passed (Test-Path $envExamplePath) -Detail $envExamplePath + +$envPath = Join-Path $saasAppPath '.env' +Write-Check -Label '.env' -Passed (Test-Path $envPath) -Detail ($(if (Test-Path $envPath) { 'vorhanden' } else { 'noch nicht angelegt' })) + +Write-Host '' +Write-Host 'Naechste Schritte:' +Write-Host '1. Falls Composer fehlt, zuerst Composer installieren.' +Write-Host '2. Mit scripts/prepare-saas-env.ps1 eine lokale .env anlegen.' +Write-Host '3. Danach saas-app konfigurieren und die Installationsanleitung in docs/installationshandbuch.md befolgen.' diff --git a/scripts/prepare-saas-env.ps1 b/scripts/prepare-saas-env.ps1 new file mode 100644 index 0000000..2742e88 --- /dev/null +++ b/scripts/prepare-saas-env.ps1 @@ -0,0 +1,23 @@ +param( + [string]$ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path, + [switch]$Force +) + +$ErrorActionPreference = 'Stop' + +$saasAppPath = Join-Path $ProjectRoot 'saas-app' +$sourcePath = Join-Path $saasAppPath '.env.example' +$targetPath = Join-Path $saasAppPath '.env' + +if (-not (Test-Path $sourcePath)) { + throw "Quelle nicht gefunden: $sourcePath" +} + +if ((Test-Path $targetPath) -and -not $Force) { + throw ".env existiert bereits. Nutze -Force, wenn sie neu erzeugt werden soll." +} + +Copy-Item -Path $sourcePath -Destination $targetPath -Force + +Write-Host "Lokale .env wurde angelegt: $targetPath" +Write-Host 'Passe jetzt DB-, Mail-, Tenancy- und OIDC-Werte an.'