diff --git a/httpdocs/PROJEKT_KONTEXT.md b/httpdocs/PROJEKT_KONTEXT.md index 591f9dd..8db0225 100644 --- a/httpdocs/PROJEKT_KONTEXT.md +++ b/httpdocs/PROJEKT_KONTEXT.md @@ -7,7 +7,7 @@ DDEV-Entwicklungsumgebung, Symfony 7.x, PHP 8.2, MariaDB 10.11. --- ## Was wir bauen -Ein internes Timetracking-Tool – zunächst nur für mich, später ggf. als SaaS. +Ein internes Timetracking-Tool – zunächst nur für mich, später als SaaS. Technisch ähnlich wie **mite.de** – wir orientieren uns stark am UI/UX von mite. --- @@ -17,41 +17,105 @@ Technisch ähnlich wie **mite.de** – wir orientieren uns stark am UI/UX von mi - **Frontend**: Twig, SCSS (Webpack Encore), Vanilla JS (keine jQuery, kein Framework) - **SCSS-Struktur**: Atoms → Components → Sections (BEM-ähnlich) - **Dev**: DDEV (Port 8456 HTTPS), PHPMyAdmin installiert -- **Kein** Symfony Forms mehr – eigene HTML-Formulare mit fetch()-API +- **Kein** Symfony Forms – eigene HTML-Formulare mit fetch()-API --- -## Datenbankstruktur (Entities) +## Multi-Mandanten-Architektur -### `User` +### Konzept +- Jeder Account (Firma) bekommt eine eigene **Subdomain**: `spawntree.testtimetracking.ddev.site` +- Jeder Account bekommt eine **eigene Tenant-Datenbank**: `db_spawntree` +- Die **Central-DB** (`db`) enthält accountübergreifende Daten: User, Account, AccountUser, Token +- Die **Tenant-DB** enthält accountspezifische Daten: Client, Project, Service, TimeEntry + +### Doctrine: Zwei Connections / Entity Manager +- `central` → `App\Entity\Central\*` (User, Account, AccountUser, InviteToken, RegistrationToken) +- `tenant` → `App\Entity\Tenant\*` (Client, Project, Service, TimeEntry) +- Tenant-Connection wechselt DB-Name zur Laufzeit via `TenantConnectionMiddleware` + +### Subdomain-Erkennung +- `TenantRequestSubscriber` (Priorität 20, vor Firewall) liest den Subdomain-Slug aus dem Host +- Findet den Account → setzt ihn in `TenantContext` +- Kein Account für Slug → Redirect zur Hauptdomain +- `TenantContext` ist ein Request-scoped Service, der den aktiven Account hält + +--- + +## Datenbankstruktur + +### Central-DB: `App\Entity\Central` + +#### `User` - `id`, `email` (unique), `firstName`, `lastName`, `password` (nullable), `note` -- Standard-User: `f.eisenmenger@spawntree.de` / Flo Eisenmenger +- Implementiert `UserInterface`, `PasswordAuthenticatedUserInterface` +- `getFullName()` → `"Flo Eisenmenger"` + +#### `Account` +- `id`, `name`, `slug` (unique, → Subdomain), `trackingInterval` (smallint, default 1) +- `createdAt`, `superAdminUser` (ManyToOne → User, nullable) +- `accountUsers` (OneToMany → AccountUser) +- `getTenantDbName()` → `"db_" . str_replace('-', '_', slug)` +- `isSuperAdmin(User)` → bool +- `getAdmins()` → Collection aktiver Admin-AccountUser + +#### `AccountUser` +- `id`, `account` (ManyToOne), `user` (ManyToOne), `role` (admin/member/tracker), `archivedAt` +- Konstanten: `ROLE_ADMIN`, `ROLE_MEMBER`, `ROLE_TRACKER` +- `isArchived()`, `isAdmin()`, `isMember()`, `isTracker()`, `isMemberOrAdmin()` +- `getRoleLabel()` → Deutsch + +#### `InviteToken` +- `id`, `token`, `account`, `email`, `firstName`, `lastName`, `role`, `createdAt` +- `isExpired()` (7 Tage) + +#### `RegistrationToken` +- Für die E-Mail-Bestätigung bei Neuregistrierung + +### Tenant-DB: `App\Entity\Tenant` -### `Client` (Kunde) +#### `Client` (Kunde) - `id`, `name`, `hourlyRate` (decimal, nullable), `note` - Hat viele `Project`s -### `Project` +#### `Project` - `id`, `name`, `client` (ManyToOne → Client), `note` -### `Service` (Leistung) +#### `Service` (Leistung) - `id`, `name`, `billable` (bool, default true), `note` -### `TimeEntry` -- `id`, `date` (DATE_IMMUTABLE), `duration` (int, **Minuten**), `user`, `project`, `service` (nullable), `note`, `createdAt`, `updatedAt` -- `toArray()` Methode für JSON-Responses +#### `TimeEntry` +- `id`, `date` (DATE_IMMUTABLE), `duration` (int, **Minuten**), `userId` (int, kein FK – cross-DB) +- `project` (ManyToOne), `service` (ManyToOne, nullable), `note`, `invoiced` (bool, default false) +- `createdAt`, `updatedAt` (via `@PreUpdate`) +- `toArray()` für JSON-Responses (inkl. projectName, clientName, serviceName, serviceBillable, invoiced) +- `getDurationFormatted()` → `"1:30"` --- ## Routing-Übersicht -### Timetracking (Hauptseite) -- `GET /` → `timetracking_week` +### Öffentliche Routen (Hauptdomain) +- `GET /` → `app_home` (HomeController – Landing Page oder Redirect) +- `GET /register` → `app_register` (Registrierungsseite) +- `POST /api/register` → `api_register` +- `POST /api/register/preview-slug` → `api_register_preview_slug` (Live-Slug-Vorschau) +- `GET /verify/{token}` → `app_verify` (E-Mail-Bestätigung) + +### Auth (Subdomain) +- `GET /login` → `app_login` +- `GET /logout` → `app_logout` +- `GET /invite/{token}` → `app_invite` (Passwort setzen nach Einladung) + +### Timetracking +- `GET /` → Redirect (je nach Login-Status) +- `GET /week` → `timetracking_week` - `GET /week/{date}` → `timetracking_week_date` - `GET /api/entries?date=Y-m-d` → Einträge für einen Tag (JSON) - `POST /api/entries` → Eintrag erstellen - `PATCH /api/entries/{id}` → Eintrag bearbeiten - `DELETE /api/entries/{id}` → Eintrag löschen +- `PATCH /api/entries/{id}/invoiced` → Abrechnungsstatus toggeln ### CRUD-Seiten - `GET /clients` → `client_index` @@ -61,23 +125,78 @@ Technisch ähnlich wie **mite.de** – wir orientieren uns stark am UI/UX von mi - `GET /services` → `service_index` - `POST /api/services`, `PATCH /api/services/{id}`, `DELETE /api/services/{id}` +### Reports +- `GET /reports/times` → `report_times` + +### Team (nur Admins) +- `GET /team` → `team_index` +- `POST /api/team/invite` → Einladung versenden +- `PATCH /api/team/{id}` → Mitglied bearbeiten +- `PATCH /api/team/{id}/archive` → Mitglied archivieren +- `PATCH /api/team/{id}/unarchive` → Mitglied reaktivieren +- `DELETE /api/team/{id}` → Mitglied entfernen +- `DELETE /api/team/invite/{id}` → Einladung löschen + +### Account-Einstellungen +- `GET /account` → `account_index` (Tab: account / user, je nach Rolle) +- `PATCH /api/account` → Name + trackingInterval (nur Admin) +- `PATCH /api/account/superadmin` → Kontoinhaber übertragen (nur aktueller Superadmin) +- `PATCH /api/account/user` → Eigene Profildaten / Passwort ändern + +--- + +## Services & Subscribers + +- `TenantContext` – hält aktiven Account für den Request +- `TenantConnectionMiddleware` – wechselt Tenant-DB-Name zur Laufzeit +- `TenantRequestSubscriber` – liest Subdomain, setzt TenantContext (Prio 20) +- `ArchivedUserChecker` – blockiert Login für archivierte User +- `ArchivedUserSubscriber` – wirft Exception bei archivierten Usern während des Requests +- `SlidingSessionSubscriber` – verlängert Session bei Aktivität +- `AccountRoleHelper` – `isAdmin()`, `isMember()`, `isTracker()` für den aktuellen User/Account +- `RegistrationService` – `startRegistration()`, `confirm(token)` – erstellt Account + DB + User +- `SlugGenerator` – generiert/prüft Slugs aus Firmennamen +- `AppExtension` / `AppExtensionRuntime` – Twig-Funktionen: `deMonths()`, `deMonthsShort()`, `deWeekdays()`, `deWeekdaysShort()` +- `AccessDeniedHandler` – leitet bei 403 auf Login um + --- ## Template-Struktur ``` templates/ -├── base.html.twig ← Basistemplate mit Nav-Include -├── _nav.html.twig ← Dunkle Top-Navigation +├── base.html.twig +├── home/ +│ └── index.html.twig ← Landing Page (Hauptdomain ohne Subdomain) +├── security/ +│ └── login.html.twig +├── registration/ +│ ├── register.html.twig +│ ├── confirmed.html.twig ← Nach E-Mail-Bestätigung +│ └── confirm_error.html.twig +├── invite/ +│ ├── set_password.html.twig ← Passwort setzen nach Einladung +│ └── error.html.twig +├── email/ +│ ├── team_invite.html.twig +│ ├── registration_welcome.html.twig +│ ├── registration_confirm.html.twig +│ └── registration_notify.html.twig ├── timetracking/ -│ ├── week.html.twig ← Hauptseite (Zeiterfassung) -│ └── _entry_row.html.twig ← Partial: einzelne Zeiteintrag-Zeile +│ ├── week.html.twig ← Hauptseite Zeiterfassung +│ └── _entry_row.html.twig ├── client/ -│ └── index.html.twig ← Kundenliste mit Inline-Edit +│ └── index.html.twig ├── project/ -│ └── index.html.twig ← Projektliste mit Inline-Edit -└── service/ - └── index.html.twig ← Leistungsliste mit Inline-Edit +│ └── index.html.twig +├── service/ +│ └── index.html.twig +├── team/ +│ └── index.html.twig ← Team-Verwaltung (nur Admins) +├── account/ +│ └── index.html.twig ← Account- + Profil-Einstellungen (Tabs) +└── report/ + └── times.html.twig ← Zeiteinträge-Report ``` --- @@ -86,23 +205,29 @@ templates/ ``` assets/styles/ -├── main.scss ← Entry Point, importiert alles +├── main.scss ← Entry Point ├── atoms/ -│ ├── _variables.scss ← Farben, Spacing, etc. +│ ├── _variables.scss │ ├── _typography.scss │ ├── _buttons.scss ← .btn, .btn-primary, .btn-secondary │ └── _inputs.scss ← .input, .select, .textarea -└── components/ - ├── _week-nav.scss ← Wochennavigation im Header - ├── _month-calendar.scss ← Monatskalender (Popup) - ├── _entry-form.scss ← Zeiterfassungs-Formular - ├── _entry-list.scss ← Eintrags-Liste inkl. Inline-Edit - ├── _duration-help.scss ← "?"-Tooltip beim Dauerfeld - ├── _main-nav.scss ← Dunkle Top-Nav - ├── _greeting.scss ← Begrüßungszeile - └── _crud.scss ← CRUD-Seiten (Kunden/Projekte/Leistungen) -sections/ - └── _timetracking.scss ← .tt-page, .tt-header, .tt-content +├── components/ +│ ├── _week-nav.scss +│ ├── _month-calendar.scss +│ ├── _entry-form.scss +│ ├── _entry-list.scss +│ ├── _duration-help.scss +│ ├── _main-nav.scss +│ ├── _greeting.scss +│ ├── _crud.scss ← Kunden/Projekte/Leistungen +│ ├── _login.scss +│ ├── _register.scss +│ ├── _team.scss +│ ├── _account.scss +│ └── _report.scss +└── sections/ + ├── _timetracking.scss ← .tt-page, .tt-header, .tt-content + └── _home.scss ← Landing Page ``` --- @@ -111,37 +236,28 @@ sections/ ``` assets/ -├── app.js ← Webpack Entry für Hauptseite -│ importiert: main.scss, calendar.js, entries.js -├── scripts/ -│ ├── calendar.js ← WeekCalendar Klasse -│ │ - Wochennavigation mit Slide-Animation -│ │ - Monatsansicht (Popup) -│ │ - Ruft window.entryManager.loadEntriesForDate() auf -│ ├── entries.js ← EntryManager Klasse -│ │ - Event Delegation auf #entry-list -│ │ - CRUD via fetch() -│ │ - localStorage für letztes Projekt/Leistung -│ │ - Importiert aus duration.js -│ ├── duration.js ← Hilfsfunktionen (Export) -│ │ - parseDuration() (1:30, 8 12, 1,75) -│ │ - roundToQuarter() (konfigurierbar) -│ │ - DURATION_CONFIG.roundToQuarter = true -│ │ - initDurationBlurHandler() -│ └── crud.js ← Webpack Entry für CRUD-Seiten -│ Generic für Kunden/Projekte/Leistungen +├── app.js ← Entry: importiert main.scss, calendar.js, entries.js +└── scripts/ + ├── calendar.js ← WeekCalendar (Wochennavigation, Monatsansicht) + ├── entries.js ← EntryManager (CRUD, fetch, localStorage) + ├── duration.js ← parseDuration(), roundToQuarter(), formatMinutes(), + │ validateDuration(), initDurationBlurHandler() + ├── crud.js ← Entry: generisches CRUD (Kunden/Projekte/Leistungen) + ├── registration.js ← Entry: Registrierungs-Flow, Live-Slug-Vorschau + ├── team.js ← Entry: Team-Verwaltung + ├── account.js ← Entry: Account- + Profil-Einstellungen + └── report.js ← Entry: Report-Seite, Edit + Invoiced-Toggle ``` -**Wichtig**: `entries.js` nutzt ES Module `import/export` – funktioniert mit Webpack Encore. - ---- - -## Webpack Encore (webpack.config.js) -Zwei Entries: -- `app` → `./assets/app.js` (Hauptseite) -- `crud` → `./assets/scripts/crud.js` (CRUD-Seiten) - -CRUD-Seiten laden `crud.js` via `{{ encore_entry_script_tags('crud') }}` im Twig-Block. +### Webpack Encore Entries +| Entry | Datei | Genutzt auf | +|---------------|------------------------------|------------------------------------| +| `app` | `assets/app.js` | Timetracking-Wochenansicht | +| `crud` | `assets/scripts/crud.js` | Client/Project/Service-Seiten | +| `registration`| `assets/scripts/registration.js` | Registrierungsseite | +| `team` | `assets/scripts/team.js` | Team-Seite | +| `account` | `assets/scripts/account.js` | Account-Einstellungen | +| `report` | `assets/scripts/report.js` | Report-Seite | --- @@ -150,50 +266,62 @@ CRUD-Seiten laden `crud.js` via `{{ encore_entry_script_tags('crud') }}` im Twig ### Durations - Gespeichert als **Integer (Minuten)** in der DB - Eingabe: `1:30`, `8 12` (von-bis), `1,75` (Dezimal), `0:00` (Reset) -- Automatisch auf **15-Minuten-Schritt aufgerundet** (konfigurierbar) +- Runden: konfigurierbar per `Account.trackingInterval` (1, 15, 30, 60 Minuten) - Anzeige: `formatMinutes()` → `"1:30"` ### Translations - Alle UI-Strings in `translations/messages.de.yaml` -- Datums-Arrays (Monate, Wochentage) in `AppExtension.php` als Twig-Functions: `deMonths()`, `deMonthsShort()`, `deWeekdays()`, `deWeekdaysShort()` -- JS bekommt alle Strings via `window.TT.i18n` (aus Twig gesetzt) -- JS-Zugriff: `function t(key) { return window.TT?.i18n?.[key] ?? key; }` +- Datums-Arrays in `AppExtension.php` als Twig-Funktionen: `deMonths()`, `deMonthsShort()`, `deWeekdays()`, `deWeekdaysShort()` +- JS bekommt Strings via `window.TT.i18n` (Timetracking) bzw. `window.Report.i18n` (Report) etc. +- JS-Zugriff: `function t(key) { return window.X?.i18n?.[key] ?? key; }` ### API-Pattern - Alle API-Routen unter `/api/...` - JSON Request/Response -- Kein CSRF (reine JSON-API) +- CSRF nur bei `form_login` (Symfony Security), sonst kein CSRF - Fehler: `{ error: "..." }` mit passendem HTTP-Status +- Validierungsfehler: `{ errors: ["..."] }` mit 422 + +### Rollen +| Rolle | Kann | +|-----------|----------------------------------------------------| +| `admin` | Alles: Team verwalten, alle Einträge sehen/bearbeiten, Account-Einstellungen | +| `member` | Eigene Einträge + alle fremden Einträge sehen (kein Team-Zugriff) | +| `tracker` | Nur eigene Einträge sehen und bearbeiten | -### Aktiver User -Aktuell hardcoded auf `f.eisenmenger@spawntree.de`. -Auth (Login/Session) ist noch **nicht gebaut** – kommt später. +### Archivierung vs. Löschen (Team) +- User mit Zeiteinträgen können nur **archiviert** werden (nicht gelöscht) +- Archivierte User können sich nicht einloggen (`ArchivedUserChecker`) +- Kontoinhaber (`superAdminUser`) kann nicht archiviert/gelöscht werden --- ## Was noch fehlt / TODO -- [ ] Login / Authentifizierung (Symfony Security) -- [ ] Reports-Seite -- [ ] Wochenübersicht mit Summen +- [ ] Filter auf Report-Seite (Datumsbereich, Projekt, Service, User) - [ ] Export (CSV / PDF) -- [ ] Multi-User / Mandantenfähigkeit - [ ] Timer-Funktion (Live-Zeiterfassung) -- [ ] Passwort-Hashing für User-Entity +- [ ] Wochenübersicht mit Summen pro Tag (im Wochenkalender) +- [ ] E-Mail-Konfiguration für Produktivbetrieb (aktuell DDEV Mailpit) +- [ ] Passwort-Reset-Flow --- -## Seed-Daten (reset-and-seed.sh) +## Seed-Daten ```bash -bash reset-and-seed.sh +bash 1-reset-and-seed.sh # → DB droppen, Migrations ausführen, app:seed aufrufen +bash 2-update-tenant-db.sh +# → Tenant-DB-Schema aktualisieren ``` -`app:seed` legt an: 1 User, 5 Leistungen (4 verrechenbar, 1 intern), 10 Kunden mit je 1-3 Projekten. +`app:seed` legt an: 1 Account (spawntree), 1 Admin-User, 5 Leistungen (4 verrechenbar, 1 intern), 10 Kunden mit je 1–3 Projekten. --- ## DDEV-Konfiguration - Projekt: `testtimetracking` -- URL: `https://testtimetracking.ddev.site:8456` -- PHPMyAdmin: `https://testtimetracking.ddev.site:8037` (nach `ddev get ddev/ddev-phpmyadmin`) -- MariaDB: User `db`, Passwort `db`, DB `db` +- Hauptdomain: `https://testtimetracking.ddev.site:8456` +- Tenant-Subdomain Beispiel: `https://spawntree.testtimetracking.ddev.site:8456` +- PHPMyAdmin: `https://testtimetracking.ddev.site:8037` +- MariaDB: User `db`, Passwort `db`, Central-DB `db` - `.env`: `DATABASE_URL="mysql://db:db@db:3306/db?serverVersion=10.11.2-MariaDB&charset=utf8mb4"` +- `APP_DOMAIN=testtimetracking.ddev.site:8456` (für Subdomain-Erkennung und E-Mail-Links)