# Potential Fix — `ajax/calc.php` — date_start_planned ignorowane

Data analizy: 2026-03-27

## Objaw

Po kliknięciu **Calc** dla FM7:
- MO2603-0001 (date_start_planned = 27/03) wypełnia capacity od 27/03 do 03/04 — zostaje 10h wolnych na 03/04 Fri
- MO2603-0004 (date_start_planned = **07/04 14:00**) ląduje na 03/04 zamiast na 07/04
- Między 03/04 a 07/04 widać „gap" w gridzie, którego nie powinno być

---

## Diagnoza — root cause

### Linia ~199: parsowanie `date_start_planned`

```php
'date_start_planned' => $row->date_start_planned
                        ? date('Y-m-d', strtotime($row->date_start_planned))
                        : null,
```

MySQL zwraca `date_start_planned` jako string `'2026-04-07 14:00:00'`.  
Jeżeli `strtotime()` zwróci **`false`** (nieoczekiwany format, problem z timezone itp.):

```
date('Y-m-d', false)  →  date('Y-m-d', 0)  →  '1970-01-01'
```

### Linia ~232: warunek startu schedulingu

```php
if (!empty($mo['date_start_planned'])) {
    $ptr = max($calcStart, $mo['date_start_planned']);
}
```

- `!empty('1970-01-01')` → **TRUE** — constraint pozornie ustawiony
- `max('2026-03-27', '1970-01-01')` → **`'2026-03-27'`** — data ze schedulingu ignorowana
- `$ptr` = dzisiaj → algorytm trafia na wolne 10h w dniu 03/04 i tam wbija MO2603-0004

---

## Plan naprawy — tylko `ajax/calc.php`

### Zmiana 1 — nowa funkcja pomocnicza `calc_parse_ymd()`

Dodać przed pętlą budowania `$moList`:

```php
/**
 * Safely parse a MySQL datetime/date string to 'Y-m-d'.
 * Returns null if the value is missing, zero-date, or unparseable.
 *
 * @param string|null $raw  Value from DB row
 * @return string|null      'Y-m-d' or null
 */
function calc_parse_ymd($raw)
{
    if (empty($raw) || substr($raw, 0, 4) === '0000') {
        return null;
    }
    // Try strict datetime format first, then date-only
    $dt = DateTime::createFromFormat('Y-m-d H:i:s', $raw)
       ?: DateTime::createFromFormat('Y-m-d', $raw);
    if ($dt === false) {
        return null; // never fall back to '1970-01-01'
    }
    return $dt->format('Y-m-d');
}
```

### Zmiana 2 — użycie nowej funkcji przy budowaniu `$moList`

```php
// STARY:
'date_start_planned' => $row->date_start_planned
                        ? date('Y-m-d', strtotime($row->date_start_planned))
                        : null,

// NOWY:
'date_start_planned' => calc_parse_ymd($row->date_start_planned),
```

### Zmiana 3 — ścisłe sprawdzenie `!== null` w pętli schedulingu

```php
// STARY (przepuszcza '1970-01-01'):
if (!empty($mo['date_start_planned'])) {

// NOWY (null = brak constraintu, wszystko inne = prawdziwa data):
if ($mo['date_start_planned'] !== null) {
```

---

## Dlaczego trzy zmiany razem?

| Problem | Zmiana 1 | Zmiana 2 | Zmiana 3 |
|---|---|---|---|
| `strtotime` zwraca `false` → `'1970-01-01'` | ✅ nowa funkcja nigdy nie produkuje `'1970-01-01'` | ✅ używa nowej funkcji | ✅ nawet gdyby coś wyciekło, `null` jest bezpieczne |
| `'0000-00-00 00:00:00'` z MySQL | ✅ explicite odrzuca | ✅ | ✅ |
| Przyszłe edge-casy formatu daty | ✅ `DateTime::createFromFormat` + fallback | ✅ | ✅ |

---

## Oczekiwany wynik po naprawie

- MO2603-0001: 27/03 Mon → 03/04 Fri (2h) — bez zmian
- MO2603-0004: **07/04 Tue** (10h) — poprawnie respektuje date_start_planned
- 03/04 Fri: 2h planned, 10h unplanned
- Gap 04/04–06/04 (Sat–Mon) — pusty zgodnie z weekendami/konfiguracją
