# CLAUDE.md – spawntree Timetracker ## Projekt-Überblick Internes Timetracking-Tool (Orientierung an mite.de), zunächst Single-User, geplant als SaaS. Multi-Tenant-Architektur: jeder Account bekommt eine Subdomain und eigene Tenant-DB. ## Tech Stack - **Backend**: Symfony 7.4, PHP 8.2+ (DDEV nutzt 8.4), Doctrine ORM, MariaDB 10.11 - **Frontend**: Twig, SCSS (Webpack Encore), Vanilla JS (kein Framework, kein jQuery) - **Export**: PhpSpreadsheet (Excel), Dompdf (PDF), natives PHP (CSV) - **Dev-Umgebung**: DDEV, Projektname `timetracking`, HTTPS-Port 8459 - **Keine** Symfony Forms – eigene HTML-Formulare mit `fetch()`-API ## Verzeichnisstruktur Der Symfony-Code liegt unter `httpdocs/`. Das ist das Projekt-Root für Symfony. ``` httpdocs/ ├── src/ │ ├── Controller/ # Symfony Controller (Routes via PHP Attributes) │ ├── Entity/ │ │ ├── Central/ # User, Account, AccountUser, Tokens (Central-DB) │ │ └── Tenant/ # Client, Project, Service, TimeEntry (Tenant-DB) │ ├── Repository/ │ │ ├── Central/ │ │ └── Tenant/ │ ├── Service/ # TenantContext, RegistrationService, etc. │ ├── Doctrine/ # TenantConnectionMiddleware │ ├── EventSubscriber/ # TenantRequestSubscriber, ArchivedUserSubscriber │ ├── Security/ # ArchivedUserChecker, AccessDeniedHandler │ └── Twig/ # AppExtension + Runtime ├── config/ │ ├── packages/ │ │ ├── doctrine.yaml # Zwei Connections: central + tenant │ │ └── security.yaml # Firewall, Access Control, form_login │ ├── services.yaml # DI: Tenant-EM explizit an Controller gebunden │ └── routes.yaml ├── templates/ # Twig-Templates (Atomic Design: atoms/components/sections) ├── assets/ │ ├── app.js # Webpack-Entry für Timetracking │ ├── styles/ # SCSS (main.scss als Entry) │ └── scripts/ # JS-Module (calendar, entries, crud, team, account, report) ├── migrations/ │ ├── central/ # Doctrine-Migrations für Central-DB │ └── tenant/ # Doctrine-Migrations für Tenant-DB ├── translations/ # messages.de.yaml └── public/ # Webroot (index.php, build/) ``` ## Multi-Tenant-Architektur - **Central-DB** (`db`): User, Account, AccountUser, Tokens - **Tenant-DB** (`db_{slug}`): Client, Project, Service, TimeEntry - `TenantRequestSubscriber` (Prio 20) liest Subdomain → setzt `TenantContext` - `TenantConnectionMiddleware` schaltet die DB-Connection auf `db_{slug}` um - Zwei Doctrine Entity Manager: `central` und `tenant` ## Wichtige Befehle ```bash # Dev-Umgebung starten ddev start # Datenbank Reset + Seed (innerhalb DDEV) ddev exec bash 1-reset-and-seed.sh ddev exec bash 2-update-tenant-db.sh # Migrations ddev exec php bin/console doctrine:migrations:migrate --em=central --no-interaction ddev exec php bin/console doctrine:migrations:migrate --em=tenant --no-interaction # Frontend Build ddev exec npm run dev # Development ddev exec npm run watch # Watch-Mode ddev exec npm run build # Production # Cache leeren ddev exec php bin/console cache:clear # Deploy bash httpdocs/deploy.sh ``` ## Webpack Encore Entries | Entry | Datei | Seite | |----------------|----------------------------------|--------------------------| | `app` | `assets/app.js` | Timetracking-Woche | | `crud` | `assets/scripts/crud.js` | Kunden/Projekte/Services | | `registration` | `assets/scripts/registration.js` | Registrierung | | `team` | `assets/scripts/team.js` | Team-Verwaltung | | `account` | `assets/scripts/account.js` | Account-Einstellungen | | `report` | `assets/scripts/report.js` | Report-Seite | ## Konventionen - **Durations**: Integer (Minuten) in der DB, Eingabeformate: `1:30`, `8 12` (von-bis), `1,75` (Dezimal) - **Rounding**: Konfigurierbar per `Account.trackingInterval` (1/15/30/60 Min) - **API-Pattern**: `/api/...` Routen, JSON Request/Response, kein CSRF auf API-Endpunkten - **Rollen**: `admin` (alles), `member` (eigene + fremde Einträge sehen), `tracker` (nur eigene) - **CSS Custom Properties**: Brand-Farben via `:root`-Variablen (`--color-primary`, etc.) ### Translations — keine hardcodierten Strings Alle UI-Texte leben zentral in `translations/messages.de.yaml`. Nirgends dürfen deutsche Strings direkt im Code stehen. - **Twig**: `{{ 'app.section.key'|trans }}` bzw. `{{ 'app.key'|trans({'%placeholder%': value}) }}` - **PHP Controller/Services**: `TranslatorInterface` injizieren, `$this->translator->trans('app.key')` nutzen - **JS**: Strings werden im Twig-Template als `window.XX.i18n`-Objekt übergeben (z.B. `window.CRUD.i18n`, `window.ACCOUNT.i18n`). JS-Module nutzen `createTranslator('XX')` aus `utils.js` zum Zugriff: `const t = createTranslator('CRUD'); t('confirmDelete')` - **Schlüsselstruktur**: `app.{section}.{key}` (z.B. `app.team.btn_new`, `app.error.access_denied`, `app.validation.email_required`) ### Kein Inline-CSS Keine `style="..."`-Attribute in Twig-Templates oder JS-generiertem HTML. Stattdessen eigene SCSS-Klassen verwenden. ### SCSS: Mixins statt Duplikation Wiederkehrende Patterns sind in `atoms/_mixins.scss` ausgelagert. Bei neuen Komponenten die vorhandenen Mixins nutzen: - `@include card($bg, $radius)` — Karte mit Schatten - `@include icon-btn($size, $shape)` — Icon-Button (rund, transparent) - `@include page-shell` — Seiten-Grundlayout (min-height, flex-column, bg) - `@include section-header` — Header-Gradient mit Flex-Layout - `@include text-truncate` — Einzeiliger Text mit Ellipsis - `@include form-label` — Formular-Label-Styling ### JS: utils.js Gemeinsame Hilfsfunktionen in `assets/scripts/utils.js`: - `esc(str)` — HTML-Escaping für dynamische Inhalte (XSS-Prävention). Immer nutzen wenn User-Daten in JS-generiertes HTML eingefügt werden. - `createTranslator(namespace)` — i18n-Zugriff (siehe Translations) - `removeWithAnimation(el, className)` / `animateIn(el, className)` — Animierte DOM-Operationen - Konstanten: `ANIMATION_MS`, `FADE_MS`, `MINUTES_PER_DAY` ### Globale `[hidden]`-Regel `main.scss` enthält `[hidden] { display: none !important; }`. Kein `&[hidden] { display: none !important; }` in einzelnen Komponenten nötig. ### PHP: `readonly` Constructor Promotion Controller-Abhängigkeiten immer mit `private readonly` deklarieren. ### Twig: Wiederverwendbare Macros Komplexe Logik die in mehreren Templates vorkommt wird in `templates/_macros/helpers.html.twig` als Macro ausgelagert (z.B. `smart_date`). ## Rollen-System | Rolle | Rechte | |-----------|------------------------------------------------------------------| | `admin` | Alles: Team, alle Einträge, Account-Settings | | `member` | Eigene Einträge + alle fremden sehen (kein Team-Zugriff) | | `tracker` | Nur eigene Einträge | Superadmin = Account-Ersteller, kann Kontoinhaber übertragen und `primaryColor` setzen. ## Services-Injection (services.yaml) Controller die Tenant-Entities nutzen brauchen den `tenant_entity_manager` explizit: - `$tenantEm: '@doctrine.orm.tenant_entity_manager'` (TimeTrackingController, ReportController) - `$em: '@doctrine.orm.tenant_entity_manager'` (ClientController, ProjectController, ServiceController) ## Report-Exporte - **Excel** (`/reports/export/excel`): PhpSpreadsheet, Autofilter, Frozen Header, Zebra-Stripes, Summenzeile - **CSV** (`/reports/export/csv`): Semikolon-Trennzeichen, UTF-8 BOM, deutsche Zahlenformatierung - **PDF** (`/reports/export/pdf`): Dompdf, A4 Querformat, professioneller Header + Footer Alle drei nutzen die gleichen Filter-Parameter wie die Report-Seite, exportieren ohne Limit. `ReportExportService` bereitet Daten zentral in `prepareData()` auf, formatspezifische Methoden erzeugen die Ausgabe. Tracker sehen nur eigene Einträge. ## TenantConnectionMiddleware Registriert via Service-Tag in `services.yaml` (nicht via `doctrine.yaml` — DoctrineBundle 3.x unterstützt `middlewares`-Config-Key nicht): ```yaml App\Doctrine\TenantConnectionMiddleware: tags: - { name: doctrine.middleware, connection: tenant } ```