Переглянути джерело

Projekt Kontext updated

master
FlorianEisenmenger 1 тиждень тому
джерело
коміт
a3423c64fd
1 змінених файлів з 210 додано та 82 видалено
  1. +210
    -82
      httpdocs/PROJEKT_KONTEXT.md

+ 210
- 82
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 13 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)

Завантаження…
Відмінити
Зберегти