Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 
 
 

13 KiB

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, LexofficeService, 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, stopwatch, searchable-select, 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

# 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
  • Timer/Stoppuhr: TimeEntry.timerStartedAt (nullable DateTimeImmutable) markiert laufende Timer. Pro User nur ein aktiver Timer gleichzeitig. Elapsed wird beim Stoppen auf trackingInterval gerundet und zu duration addiert.
  • Stundensätze (hourlyRate): Dreistufige Kaskade Project.hourlyRate → Client.hourlyRate → Service.hourlyRate (via COALESCE). Kunden und Projekte haben einen Rate-Mode-Toggle (Standard-Sätze vs. eigener Stundensatz), Services immer ein direktes Feld. Revenue-Berechnung in TimeEntryRepository::sumRevenueFiltered() nutzt die Kaskade.
  • 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, window.STOPWATCH.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

JS: searchable-select.js

Wiederverwendbares durchsuchbares Dropdown-Modul (SearchableSelect-Klasse) mit Gruppen, Keyboard-Navigation, Filter und onSelect-Callback. Wird von stopwatch.js (Projekt/Service-Auswahl) und crud.js (Lexoffice-Kontaktauswahl) importiert.

JS: stopwatch.js

Stoppuhr-Modul:

  • StopwatchManager — Timer-Steuerung: Start/Stop/Resume, Tick-Display (Sekunden-Auflösung), DOM-Integration mit window.entryManager, Tab-Title-Update

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, primaryColor und lexofficeApiKey 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.

Stoppuhr / Timer

Live-Timer zum Tracken von Zeiteinträgen. UI in der Navigation (Desktop + Hamburger-Menü), zusätzlich Play-Button an jeder Entry-Row zum Fortsetzen.

Architektur

  • Backend: Timer-API im TimeTrackingController (/api/timer/*)
  • Frontend: assets/scripts/stopwatch.js (importiert in app.js), nutzt SearchableSelect aus searchable-select.js
  • Styles: assets/styles/components/_stopwatch.scss
  • Template: Popover in _sections/nav.html.twig, Play-Button in timetracking/_entry_row.html.twig
  • Icon: _atoms/icon-stopwatch.html.twig

Timer-API Endpunkte

Route Method Beschreibung
/api/timer/status GET Prüft ob ein Timer läuft
/api/timer/options GET Projekte + Services für Select-Dropdowns
/api/timer/start POST Neuen Timer starten (erstellt TimeEntry)
/api/timer/start/{id} POST Bestehenden Eintrag fortsetzen (Resume)
/api/timer/stop POST Laufenden Timer stoppen + Duration addieren

Timer-Logik

  • TimeEntry.timerStartedAt wird beim Start gesetzt, beim Stopp auf null
  • Beim Stopp: elapsed = now - timerStartedAt, gerundet auf Account.trackingInterval, addiert zu duration
  • Maximal 1440 Min/Tag (Overflow-Schutz)
  • Conflict (409): wenn bereits ein Timer läuft, User wird gefragt ob der alte gestoppt werden soll
  • LocalStorage (tt_timer_state): persistiert Timer-State über Page-Reloads, wird mit Server-State abgeglichen
  • LocalStorage (tt_last_project_id, tt_last_service_id): merkt letzte Auswahl für Quick-Start

Lexoffice-Integration (Lexware Office)

Optionale Verknüpfung von Kunden mit Lexware Office Kontakten. Nur aktiv wenn Account.lexofficeApiKey gesetzt ist.

Datenmodell

  • Account.lexofficeApiKey (Central-DB): API-Key für Lexware Office, gespeichert pro Account
  • Client.lexofficeContactId (Tenant-DB): UUID des verknüpften Lexware-Kontakts (nullable)

Architektur

  • Service: LexofficeService — HTTP-Client für Lexware Office API (https://api.lexware.io/v1), paginierter Kontakt-Abruf mit Rate-Limit-Retry
  • Controller: LexofficeController — API-Endpunkte für Kontaktlisten und Einzelkontakte
  • Client-Controller: lexoffice-refresh-Endpunkt zum Aktualisieren des Kundennamens aus Lexware
  • Frontend: Lexoffice-Logik in crud.js, nutzt SearchableSelect für Kontakt-Dropdown
  • Account-Settings: API-Key-Verwaltung im Account-Tab (Eingabe/Ändern mit Maskierung)

API-Endpunkte

Route Method Beschreibung
/api/lexoffice/contacts GET Alle Kunden-Kontakte aus Lexware Office
/api/lexoffice/contacts/{contactId} GET Einzelnen Kontakt abrufen
/api/clients/{id}/lexoffice-refresh PATCH Kundenname aus Lexware aktualisieren

Verhalten

  • Beim Verknüpfen wird der Kundenname aus Lexware übernommen, das Name-Feld wird disabled
  • Bereits verknüpfte Kontakte werden in der Dropdown-Liste ausgeblendet (Duplikat-Schutz)
  • Reload-Button pro Zeile zum Aktualisieren des Namens aus Lexware
  • Kontakte werden clientseitig gecacht (einmal geladen pro Page-Session)

TenantConnectionMiddleware

Registriert via Service-Tag in services.yaml (nicht via doctrine.yaml — DoctrineBundle 3.x unterstützt middlewares-Config-Key nicht):

App\Doctrine\TenantConnectionMiddleware:
    tags:
        - { name: doctrine.middleware, connection: tenant }