# ChronosLog Time Tracking API

Basis-URL: `/api/v1`

Die API ist für iOS, Android, Web-Terminals und das Portal identisch. Mobile Apps authentifizieren sich mit OAuth/Session-Token, reine Terminalgeräte zusätzlich über eine Terminal-ID. Mitarbeiter-PINs werden nur zum Auslösen einer Zeitbuchung verwendet und niemals im Portal-Login.

Das Portal ist mandantenfähig. Jede verkaufte Firma erhält eine eigene nicht erratbare Firmenkennung als `tenantId`, zum Beispiel `BL-7K3Q9` für Brügges Loui. Zu jeder Firmenkennung gehören eigene Login-Zugänge, eine eigene Datenbankkennung, eigene API-Schlüssel, eigene Terminalgeräte und eigene Backups. Ein normaler Firmenzugang sieht keinen Umschalter und keine anderen Firmen.

Empfohlener Header für Apps und Portal:

```http
X-Tenant-Id: BL-7K3Q9
```

Alternativ kann die Tenant-Zuordnung aus dem Token kommen. Das ist für Firmenzugänge der Standard: Login mit Firmenkennung, E-Mail und Passwort ergibt genau einen Mandanten.

## Firmen bereitstellen

Diese Endpunkte gehören in den Betreiber-/Systemadmin-Bereich, nicht in das normale Kundenportal.

`GET /tenants`

Antwort:

```json
{
  "tenants": [
    {
      "id": "BL-7K3Q9",
      "name": "Brügges Loui",
      "database": "tenant_bl_7k3q9_prod",
      "backupPolicy": "daily",
      "backupStatus": "healthy"
    }
  ]
}
```

`POST /tenants`

```json
{
  "id": "BL-7K3Q9",
  "name": "Brügges Loui",
  "legalName": "Hotel Brügges Loui",
  "address": "Hochsauerlandstraße 16, 34508 Willingen (Upland)",
  "phone": "05632-5019",
  "email": "info@brueggesloui.de",
  "manager": "Karl-Heinz Jäger"
}
```

Beim Anlegen einer Firma erzeugt das Backend eine eigene Datenbankkennung, eine eigene Backup-Konfiguration und getrennte Initialschlüssel.

## Authentifizierung

`POST /auth/login`

```json
{
  "email": "mschmirler@smp-edv.de",
  "password": "********"
}
```

Antwort:

```json
{
  "accessToken": "eyJ...",
  "refreshToken": "rt_...",
  "user": {
    "id": "usr_123",
    "role": "admin",
    "organizationId": "org_123"
  }
}
```

## Aktuelle Schicht abrufen

`GET /employees/{employeeId}/current-shift`

Antwort:

```json
{
  "employeeId": "emp_123",
  "status": "working",
  "shiftId": "shift_789",
  "startedAt": "2026-05-01T08:00:00+02:00",
  "breakStartedAt": null,
  "workedMinutes": 245,
  "breakMinutes": 30
}
```

## Zeitbuchung erstellen

`POST /time-events`

Für iOS nutzt die App `employeeId`. Für Terminalgeräte kann stattdessen `pin` übermittelt werden.

```json
{
  "employeeId": "emp_123",
  "tenantId": "BL-7K3Q9",
  "type": "clock_in",
  "occurredAt": "2026-05-01T12:21:00+02:00",
  "source": "ios",
  "deviceId": "ios_device_abc",
  "location": {
    "latitude": 52.520008,
    "longitude": 13.404954,
    "accuracyMeters": 18
  },
  "idempotencyKey": "A8F7E8D4-2D7D-4B7E-9E19-123456789ABC"
}
```

Erlaubte `type` Werte:

- `clock_in`
- `break_start`
- `break_end`
- `clock_out`

Antwort:

```json
{
  "id": "evt_123",
  "employeeId": "emp_123",
  "shiftId": "shift_789",
  "type": "clock_in",
  "status": "accepted",
  "occurredAt": "2026-05-01T12:21:00+02:00",
  "requiresApproval": false
}
```

## Offline-Sync für iOS

`POST /time-events/batch`

```json
{
  "events": [
    {
      "employeeId": "emp_123",
      "type": "break_start",
      "occurredAt": "2026-05-01T12:00:00+02:00",
      "source": "ios",
      "deviceId": "ios_device_abc",
      "idempotencyKey": "offline-BL-7K3Q9"
    }
  ]
}
```

Die API verarbeitet Events idempotent. Wiederholte Requests mit gleicher `idempotencyKey` liefern dasselbe Ergebnis.

## Terminal-PIN prüfen und buchen

Terminal-Apps authentifizieren sich nicht mit einem Portal-Benutzer, sondern mit Terminal-Key:

```http
X-Tenant-Id: BL-7K3Q9
Authorization: Terminal term_bl_7k3q9_...
```

## Terminal-Konfiguration laden

`GET /terminals/{terminalId}/configuration`

Antwort:

```json
{
  "tenantId": "BL-7K3Q9",
  "terminalId": "terminal-01",
  "companyName": "Brügges Loui",
  "allowOfflineBuffer": true
}
```

## Mitarbeiter per PIN anmelden

`POST /terminals/{terminalId}/pin-session`

Die iPad-App nutzt diesen Endpunkt, sobald ein Mitarbeiter seine PIN eingibt. Die Antwort enthält nur Informationen, die am Terminal sichtbar sein dürfen.

```json
{
  "tenantId": "BL-7K3Q9",
  "pin": "2468"
}
```

Antwort:

```json
{
  "employeeId": "emp_123",
  "displayName": "Max Mustermann",
  "area": "Service",
  "status": "Aktiv",
  "lastEvents": [
    {
      "id": "evt_123",
      "type": "clock_in",
      "label": "Kommen",
      "occurredAt": "2026-05-01 08:01"
    }
  ],
  "messages": [
    {
      "id": "msg_123",
      "title": "Dienstplan",
      "body": "Bitte die neue Schichtplanung für das Wochenende beachten.",
      "createdAt": "2026-05-01 10:30"
    }
  ]
}
```

## Terminal-Zeitbuchung

`POST /terminals/{terminalId}/time-events`

```json
{
  "tenantId": "BL-7K3Q9",
  "pin": "2468",
  "type": "clock_in",
  "occurredAt": "2026-05-01T12:21:00+02:00",
  "idempotencyKey": "terminal-01-20260501-1221"
}
```

## Admin-Endpunkte

- `GET /admin/users`
- `POST /admin/users`
- `PATCH /admin/users/{userId}`
- `GET /employees`
- `POST /employees`
- `PATCH /employees/{employeeId}`
- `GET /vacations`
- `POST /vacations`
- `PATCH /vacations/{vacationId}`
- `DELETE /vacations/{vacationId}`
- `GET /schedule`
- `POST /schedule`
- `PATCH /schedule/{scheduleId}`
- `GET /time-events`
- `POST /time-events`
- `GET /tenants`
- `POST /tenants`
- `PATCH /tenants/{tenantId}`
- `GET /tenants/{tenantId}/backup-status`
- `POST /tenants/{tenantId}/backup-restore`
- `GET /admin/api-keys`
- `POST /admin/api-keys`
- `POST /admin/api-keys/{keyId}/rotate`
- `GET /terminals/{terminalId}/configuration`
- `PATCH /terminals/{terminalId}/configuration`
- `POST /terminals/{terminalId}/pin-session`
- `POST /terminals/{terminalId}/rotate-key`
- `GET /settings/time-tracking`
- `PATCH /settings/time-tracking`
- `GET /settings/smtp`
- `PATCH /settings/smtp`
- `POST /settings/smtp/test`
- `GET /backups`
- `POST /backups`
- `GET /backups/{backupId}/download`
- `GET /settings/backup`
- `PATCH /settings/backup`

## Urlaub und Abwesenheiten

`GET /vacations`

Antwort:

```json
{
  "vacations": [
    {
      "id": "vac_123",
      "employeeId": "emp_123",
      "type": "Urlaub",
      "start": "2026-05-18",
      "end": "2026-05-22",
      "status": "Beantragt",
      "note": "Sommerurlaub"
    }
  ]
}
```

`POST /vacations` und `PATCH /vacations/{vacationId}` nutzen denselben Payload.

## Dienstplan

`GET /schedule`

Antwort:

```json
{
  "schedule": [
    {
      "id": "sch_123",
      "employeeId": "emp_123",
      "area": "Service",
      "date": "2026-05-18",
      "start": "08:00",
      "end": "16:00",
      "type": "Dienst",
      "note": "Frühdienst"
    }
  ]
}
```

Mehrere Mitarbeitende in derselben Schicht werden als mehrere Einträge mit gleichem Bereich, Datum, Uhrzeit, Typ und Notiz gespeichert. Das Portal gruppiert diese Einträge als gemeinsame Schicht.

## Manuelle Korrekturen

`GET /time-events` liefert die letzten Zeitbuchungen für Prüfung und Auswertung.

`POST /time-events`

```json
{
  "id": "manual_123",
  "employeeId": "emp_123",
  "type": "clock_out",
  "label": "Gehen",
  "occurredAt": "2026-05-18T17:02:00",
  "source": "manual"
}
```

Dieser Endpunkt wird vom Portal für Nachbesserungen genutzt, wenn Kommen, Pause, Gehen oder eine Korrektur fehlt.

## SMTP Einstellungen

`PATCH /settings/smtp`

```json
{
  "tenantId": "BL-7K3Q9",
  "host": "smtp.brueggesloui.de",
  "port": 587,
  "security": "STARTTLS",
  "sender": "zeiterfassung@brueggesloui.de",
  "username": "zeiterfassung@brueggesloui.de",
  "password": "********"
}
```

## Backup Einstellungen

`PATCH /settings/backup`

```json
{
  "tenantId": "BL-7K3Q9",
  "cron": "15 2 * * *",
  "retentionDays": 30,
  "target": "S3 EU-Central · tenant-bl-7k3q9"
}
```

`GET /backups`

```json
{
  "tenantId": "BL-7K3Q9",
  "backups": [
    {
      "id": "bak_20260501_0215",
      "createdAt": "2026-05-01T02:15:00+02:00",
      "file": "tenant_bl_7k3q9_2026-05-01.sql.gz",
      "sizeBytes": 18874368,
      "status": "ok"
    }
  ]
}
```

## iOS Swift Integration

```swift
struct TimeEventRequest: Codable {
    let employeeId: String
    let type: String
    let occurredAt: String
    let source: String
    let deviceId: String
    let idempotencyKey: String
}

func clockIn(token: String, employeeId: String) async throws {
    var request = URLRequest(url: URL(string: "https://api.brueggesloui.de/api/v1/time-events")!)
    request.httpMethod = "POST"
    request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try JSONEncoder().encode(TimeEventRequest(
        employeeId: employeeId,
        type: "clock_in",
        occurredAt: ISO8601DateFormatter().string(from: Date()),
        source: "ios",
        deviceId: UIDevice.current.identifierForVendor?.uuidString ?? "unknown",
        idempotencyKey: UUID().uuidString
    ))

    let (_, response) = try await URLSession.shared.data(for: request)
    guard (response as? HTTPURLResponse)?.statusCode == 201 else {
        throw URLError(.badServerResponse)
    }
}
```
