| @@ -7,7 +7,7 @@ DDEV-Entwicklungsumgebung, Symfony 7.x, PHP 8.2, MariaDB 10.11. | |||||
| --- | --- | ||||
| ## Was wir bauen | ## 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. | 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) | - **Frontend**: Twig, SCSS (Webpack Encore), Vanilla JS (keine jQuery, kein Framework) | ||||
| - **SCSS-Struktur**: Atoms → Components → Sections (BEM-ähnlich) | - **SCSS-Struktur**: Atoms → Components → Sections (BEM-ähnlich) | ||||
| - **Dev**: DDEV (Port 8456 HTTPS), PHPMyAdmin installiert | - **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` | - `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` | - `id`, `name`, `hourlyRate` (decimal, nullable), `note` | ||||
| - Hat viele `Project`s | - Hat viele `Project`s | ||||
| ### `Project` | |||||
| #### `Project` | |||||
| - `id`, `name`, `client` (ManyToOne → Client), `note` | - `id`, `name`, `client` (ManyToOne → Client), `note` | ||||
| ### `Service` (Leistung) | |||||
| #### `Service` (Leistung) | |||||
| - `id`, `name`, `billable` (bool, default true), `note` | - `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 | ## 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 /week/{date}` → `timetracking_week_date` | ||||
| - `GET /api/entries?date=Y-m-d` → Einträge für einen Tag (JSON) | - `GET /api/entries?date=Y-m-d` → Einträge für einen Tag (JSON) | ||||
| - `POST /api/entries` → Eintrag erstellen | - `POST /api/entries` → Eintrag erstellen | ||||
| - `PATCH /api/entries/{id}` → Eintrag bearbeiten | - `PATCH /api/entries/{id}` → Eintrag bearbeiten | ||||
| - `DELETE /api/entries/{id}` → Eintrag löschen | - `DELETE /api/entries/{id}` → Eintrag löschen | ||||
| - `PATCH /api/entries/{id}/invoiced` → Abrechnungsstatus toggeln | |||||
| ### CRUD-Seiten | ### CRUD-Seiten | ||||
| - `GET /clients` → `client_index` | - `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` | - `GET /services` → `service_index` | ||||
| - `POST /api/services`, `PATCH /api/services/{id}`, `DELETE /api/services/{id}` | - `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 | ## Template-Struktur | ||||
| ``` | ``` | ||||
| templates/ | 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/ | ├── timetracking/ | ||||
| │ ├── week.html.twig ← Hauptseite (Zeiterfassung) | |||||
| │ └── _entry_row.html.twig ← Partial: einzelne Zeiteintrag-Zeile | |||||
| │ ├── week.html.twig ← Hauptseite Zeiterfassung | |||||
| │ └── _entry_row.html.twig | |||||
| ├── client/ | ├── client/ | ||||
| │ └── index.html.twig ← Kundenliste mit Inline-Edit | |||||
| │ └── index.html.twig | |||||
| ├── project/ | ├── 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/ | assets/styles/ | ||||
| ├── main.scss ← Entry Point, importiert alles | |||||
| ├── main.scss ← Entry Point | |||||
| ├── atoms/ | ├── atoms/ | ||||
| │ ├── _variables.scss ← Farben, Spacing, etc. | |||||
| │ ├── _variables.scss | |||||
| │ ├── _typography.scss | │ ├── _typography.scss | ||||
| │ ├── _buttons.scss ← .btn, .btn-primary, .btn-secondary | │ ├── _buttons.scss ← .btn, .btn-primary, .btn-secondary | ||||
| │ └── _inputs.scss ← .input, .select, .textarea | │ └── _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/ | 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 | ### Durations | ||||
| - Gespeichert als **Integer (Minuten)** in der DB | - Gespeichert als **Integer (Minuten)** in der DB | ||||
| - Eingabe: `1:30`, `8 12` (von-bis), `1,75` (Dezimal), `0:00` (Reset) | - 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"` | - Anzeige: `formatMinutes()` → `"1:30"` | ||||
| ### Translations | ### Translations | ||||
| - Alle UI-Strings in `translations/messages.de.yaml` | - 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 | ### API-Pattern | ||||
| - Alle API-Routen unter `/api/...` | - Alle API-Routen unter `/api/...` | ||||
| - JSON Request/Response | - JSON Request/Response | ||||
| - Kein CSRF (reine JSON-API) | |||||
| - CSRF nur bei `form_login` (Symfony Security), sonst kein CSRF | |||||
| - Fehler: `{ error: "..." }` mit passendem HTTP-Status | - 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 | ## 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) | - [ ] Export (CSV / PDF) | ||||
| - [ ] Multi-User / Mandantenfähigkeit | |||||
| - [ ] Timer-Funktion (Live-Zeiterfassung) | - [ ] 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 | ||||
| bash reset-and-seed.sh | |||||
| bash 1-reset-and-seed.sh | |||||
| # → DB droppen, Migrations ausführen, app:seed aufrufen | # → 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 | ## DDEV-Konfiguration | ||||
| - Projekt: `testtimetracking` | - 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"` | - `.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) | |||||