# Task: Preiserhöhung 2026 — pms_service_positions & material_catalog

## Kontext

Die Geschäftsführung hat die Preiserhöhung genehmigt.
Die WEKA-Preise stammen aus 2022 — seither sind Baupreise in Berlin/Brandenburg
um ca. +15–20 % gestiegen.

**Vor der Ausführung:** Datenbank-Backup erstellen!

```bash
php artisan tinker
# Aktuelle Preise prüfen:
DB::table('pms_service_positions')->where('unit_price', '>', 0)->avg('unit_price');
DB::table('material_catalog')->where('list_price', '>', 0)->avg('list_price');
```

---

## 1. Backup zuerst!

```bash
# In MySQL / HeidiSQL vorher ausführen:
CREATE TABLE pms_service_positions_backup_2026 AS SELECT * FROM pms_service_positions;
CREATE TABLE material_catalog_backup_2026 AS SELECT * FROM material_catalog;
```

---

## 2. Leistungspositionen — UPDATE nach Gewerk

```sql
-- ROHBAU (+8%): Erd-, Mauer-, Beton-, Zimmer-, Stahlbau
UPDATE pms_service_positions
SET unit_price = ROUND(unit_price * 1.08, 2)
WHERE trade_nr IN ('002','012','013','014','015','016','017','018')
  AND unit_price > 0;

-- DACH (+12%): Dachdecker, Abdichtung, Klempner
UPDATE pms_service_positions
SET unit_price = ROUND(unit_price * 1.12, 2)
WHERE trade_nr IN ('020','021','022')
  AND unit_price > 0;

-- AUSBAU (+10%): Putz, Estrich, Fliesen, Maler, Trockenbau, Böden
UPDATE pms_service_positions
SET unit_price = ROUND(unit_price * 1.10, 2)
WHERE trade_nr IN ('023','024','025','026','027','028','029','030',
                   '031','032','034','036','037','039')
  AND unit_price > 0;

-- TGA (+12%): Heizung, Sanitär, Elektro, Lüftung
UPDATE pms_service_positions
SET unit_price = ROUND(unit_price * 1.12, 2)
WHERE trade_nr IN ('040','041','042','044','045','046',
                   '050','053','054','058','060','061','063','075')
  AND unit_price > 0;

-- SONSTIGE (+10%): alle restlichen Gewerke mit Preis
UPDATE pms_service_positions
SET unit_price = ROUND(unit_price * 1.10, 2)
WHERE trade_nr NOT IN (
    '002','012','013','014','015','016','017','018',
    '020','021','022',
    '023','024','025','026','027','028','029','030',
    '031','032','034','036','037','039',
    '040','041','042','044','045','046',
    '050','053','054','058','060','061','063','075'
)
  AND unit_price > 0;
```

---

## 3. Materialkatalog — UPDATE

```sql
-- Alle Materialpreise +10% (Listpreis)
UPDATE material_catalog
SET list_price = ROUND(list_price * 1.10, 2)
WHERE list_price > 0;

-- Einkaufspreise ebenfalls +10% (falls vorhanden)
UPDATE material_catalog
SET purchase_price = ROUND(purchase_price * 1.10, 2)
WHERE purchase_price > 0;
```

---

## 4. Prüfung nach Update

```sql
-- Gewerk-Übersicht: Durchschnittspreise vor/nach
SELECT
    c.name AS gewerk,
    COUNT(*) AS positionen,
    ROUND(AVG(p.unit_price), 2) AS avg_preis,
    ROUND(MIN(p.unit_price), 2) AS min_preis,
    ROUND(MAX(p.unit_price), 2) AS max_preis
FROM pms_service_positions p
JOIN pms_service_position_categories c ON c.id = p.category_id
WHERE p.unit_price > 0 AND p.is_deleted = 0
GROUP BY c.id, c.name
ORDER BY c.trade_nr;

-- Materialkatalog Übersicht
SELECT
    COUNT(*) AS artikel,
    ROUND(AVG(list_price), 2) AS avg_listpreis,
    ROUND(MIN(list_price), 2) AS min,
    ROUND(MAX(list_price), 2) AS max
FROM material_catalog
WHERE list_price > 0 AND is_deleted = 0;
```

---

## 5. Via Laravel Migration (empfohlen für Nachvollziehbarkeit)

Alternativ als saubere Migration:

```bash
php artisan make:migration update_prices_2026
```

```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;

return new class extends Migration
{
    public function up(): void
    {
        // Backup-Zeitstempel in app_settings speichern
        DB::table('app_settings')->updateOrInsert(
            ['key' => 'price_update_2026'],
            ['value' => now()->toDateTimeString()]
        );

        // Rohbau +8%
        DB::statement("UPDATE pms_service_positions
            SET unit_price = ROUND(unit_price * 1.08, 2)
            WHERE trade_nr IN ('002','012','013','014','015','016','017','018')
              AND unit_price > 0");

        // Dach +12%
        DB::statement("UPDATE pms_service_positions
            SET unit_price = ROUND(unit_price * 1.12, 2)
            WHERE trade_nr IN ('020','021','022')
              AND unit_price > 0");

        // Ausbau +10%
        DB::statement("UPDATE pms_service_positions
            SET unit_price = ROUND(unit_price * 1.10, 2)
            WHERE trade_nr IN ('023','024','025','026','027','028','029','030',
                               '031','032','034','036','037','039')
              AND unit_price > 0");

        // TGA +12%
        DB::statement("UPDATE pms_service_positions
            SET unit_price = ROUND(unit_price * 1.12, 2)
            WHERE trade_nr IN ('040','041','042','044','045','046',
                               '050','053','054','058','060','061','063','075')
              AND unit_price > 0");

        // Sonstige +10%
        DB::statement("UPDATE pms_service_positions
            SET unit_price = ROUND(unit_price * 1.10, 2)
            WHERE trade_nr NOT IN (
                '002','012','013','014','015','016','017','018',
                '020','021','022',
                '023','024','025','026','027','028','029','030',
                '031','032','034','036','037','039',
                '040','041','042','044','045','046',
                '050','053','054','058','060','061','063','075'
            ) AND unit_price > 0");

        // Material +10%
        DB::statement("UPDATE material_catalog
            SET list_price = ROUND(list_price * 1.10, 2)
            WHERE list_price > 0");

        DB::statement("UPDATE material_catalog
            SET purchase_price = ROUND(purchase_price * 1.10, 2)
            WHERE purchase_price > 0");
    }

    public function down(): void
    {
        // Rückgängig: Backup-Tabellen zurückspielen
        // DB::statement("INSERT INTO pms_service_positions SELECT * FROM pms_service_positions_backup_2026");
        // Hinweis: down() nur manuell via Backup rückgängig machen!
    }
};
```

```bash
php artisan migrate
```

Dann live abrufbar unter:
```
https://bau-mgt.spielplatzmanufaktur.de/migrate
```

---

## 6. Zusammenfassung der Faktoren

| Bereich | Gewerke | Faktor |
|---|---|---|
| Rohbau | Erdarbeiten, Maurer, Beton, Zimmer, Stahl | ×1.08 (+8%) |
| Dach | Dachdecker, Abdichtung, Klempner | ×1.12 (+12%) |
| Ausbau | Putz, Estrich, Fliesen, Maler, Böden | ×1.10 (+10%) |
| TGA | Heizung, Sanitär, Elektro, Lüftung | ×1.12 (+12%) |
| Sonstige | Alle übrigen | ×1.10 (+10%) |
| Material (Listpreis) | Alle 757 Artikel | ×1.10 (+10%) |
| Material (Einkauf) | Alle 757 Artikel | ×1.10 (+10%) |

---

## Regeln

- Erst Backup, dann UPDATE
- `unit_price > 0` — Positionen ohne Preis (0.00) werden nicht verändert
- Faktoren werden nur einmal angewendet — NICHT mehrfach ausführen!
- Nach Ausführung: Prüf-Query laufen lassen und Ergebnis screenshotten

---

## Implementierungsoptionen (Empfehlung Opus 4.7)

### Option A — Einfach: Migration ausführen (wie oben, Abschnitt 5)
- Raw SQL via `DB::statement()` in Laravel Migration
- Backup-Tabellen manuell vorher anlegen
- Kein UI, kein Audit-Log, kein Rollback außer Backup
- **Risiko:** Fehler = manuelles Zurückspielen aus Backup

### Option B — Sicher: Migration + Audit-Log
- Vor dem Update: alte Preise in `pms_price_update_items` speichern
- Rollback-Migration liest aus dem Audit zurück
- Kein UI nötig, läuft via `php artisan migrate`
- **Empfohlen wenn:** einmalige Aktion, kein zukünftiges UI geplant

### Option C — Vollständig: UI + Preview + Rollback (Opus MVP)
- Neue Tabellen: `pms_price_updates` + `pms_price_update_items`
- Controller: `PriceUpdateController` (preview → apply → revert)
- View: Formular mit AJAX-Vorschau (alt → neu → Diff), Bestätigungsmodal
- Rollback-Button auf Detailseite
- **Empfohlen wenn:** Preiserhöhung jährlich wiederkehrt

### Entscheidung
| | A | B | C |
|---|---|---|---|
| Aufwand | 1h | 3h | 2 Tage |
| Sicherheit | ⚠️ | ✅ | ✅✅ |
| Wiederholbar | Nein | Nein | Ja |
| UI | Nein | Nein | Ja |
