# Jadwal Hari Ini Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add a standalone public page `GET /jadwal-hari-ini` that renders today's college schedule as a 50-minute-slot matrix (rows = time, columns = rooms, cells = `{kelas} ({dosen}) {mata_kuliah}`), with the current slot highlighted, auto-refreshing every 60s.

**Architecture:** Server-rendered Blade. `JadwalController@index` runs three small queries against `jadwal_ruangan` (no reservations, no `jadwal_gabungan` view), builds a `[kelas_id][jam_masuk H:i][dosen]` cell map in PHP, and renders a plain `<table>` with `rowspan` merging for multi-SKS cells and `$skip` counters in Blade to avoid double-emitting cells. Auto-refresh via `<meta http-equiv="refresh" content="60">`. No DataTables, no JS bundles.

**Tech Stack:** Laravel 11 (PHP 8.2), Blade, Tailwind, Carbon 2.x, PHPUnit 11, sqlite in-memory for tests.

## File Structure

| File | Type | Responsibility |
|---|---|---|
| `phpunit.xml` | edit | Enable sqlite in-memory for tests (currently commented out) |
| `app/Http/Controllers/JadwalController.php` | **NEW** | One `index()` action: queries + cell map + view render |
| `resources/views/jadwal-hari-ini.blade.php` | **NEW** | Matrix table with rowspan merging, current-slot highlight, empty-day fallback |
| `routes/web.php` | edit | Add `GET /jadwal-hari-ini` route |
| `resources/views/app/index.blade.php` | edit | Add a header link "Lihat Jadwal Hari Ini" |
| `tests/Feature/JadwalHariIniTest.php` | **NEW** | Feature tests covering route, data layer, view, current-slot highlight, empty day, link |
| `docs/superpowers/plans/2026-07-29-jadwal-hari-ini.md` | **NEW** | This plan |

## Global Constraints

- Source: only `jadwal_ruangan` (the `tetap` schedule). `reservasi_ruangan` is **excluded**.
- Slot width: 50 minutes.
- Cell text format per entry: `{kelas} ({dosen}) {mata_kuliah_nama}` (single line per entry, multiple entries stacked when grouped).
- Cell grouping key: `(kelas_id, jam_masuk, jam_keluar, dosen)`.
- Current-slot highlight: row whose `HH:MM` label matches `now('Asia/Jakarta')->format('H:i')` rounded to the nearest 50-min boundary, gets `!bg-green/15`.
- Room column header: `kode_ruangan` (bold) on top, `nama` (`text-[10px] text-gray-500`) underneath. Ordered by `kode_ruangan` ASC.
- Time row label: `HH.MM - HH.MM` (dot separator).
- Empty cell: `—` (em dash) in `text-gray-400`.
- Empty day: render "Tidak ada jadwal hari ini." centered message in the same card.
- Auto-refresh: `<meta http-equiv="refresh" content="60">` in `<head>`.
- Wrapper: `<x-guest-layout>`.
- Card style: `bg-white rounded-xl p-4 shadow-sm ring-1 ring-gray-950/5` (matches `data/jadwal.blade.php`).
- The "IF -" prefix from the reference image is **out of scope**.
- Reservations (`tidak tetap`) are **out of scope**.
- The existing `JadwalRuanganDataTable` is **untouched**.
- No new models, no migrations, no npm packages.

---

### Task 1: Enable sqlite in-memory for tests + add the route + minimal controller + minimal view

**Files:**
- Modify: `phpunit.xml` (uncomment the sqlite `<env>` lines)
- Modify: `routes/web.php` (add the route)
- Create: `app/Http/Controllers/JadwalController.php`
- Create: `resources/views/jadwal-hari-ini.blade.php`
- Create: `tests/Feature/JadwalHariIniTest.php`

**Interfaces:**
- Produces: `GET /jadwal-hari-ini` returns HTTP 200 with body containing "Jadwal Perkuliahan Hari Ini"

- [ ] **Step 1: Update `phpunit.xml` to use sqlite in-memory**

Replace lines 25-26:
```xml
        <!-- <env name="DB_CONNECTION" value="sqlite"/> -->
        <!-- <env name="DB_DATABASE" value=":memory:"/> -->
```
with:
```xml
        <env name="DB_CONNECTION" value="sqlite"/>
        <env name="DB_DATABASE" value=":memory:"/>
```

- [ ] **Step 2: Write the failing feature test**

Create `tests/Feature/JadwalHariIniTest.php`:

```php
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class JadwalHariIniTest extends TestCase
{
    use RefreshDatabase;

    public function test_page_returns_200_and_shows_title(): void
    {
        $response = $this->get('/jadwal-hari-ini');

        $response->assertStatus(200);
        $response->assertSee('Jadwal Perkuliahan Hari Ini');
    }
}
```

- [ ] **Step 3: Run the test to confirm it fails (404)**

Run: `php artisan test --filter=JadwalHariIniTest::test_page_returns_200_and_shows_title`
Expected: FAIL with `404` (route not defined)

- [ ] **Step 4: Add the route to `routes/web.php`**

At the end of `routes/web.php` (after line 92, the `Route::post('/reservasi/{id}'...` line), add:

```php
    Route::get('/jadwal-hari-ini', [JadwalController::class, 'index'])->name('jadwal.hari-ini');
```

Also add the use statement at the top, next to the other controller imports:

```php
use App\Http\Controllers\JadwalController;
```

- [ ] **Step 5: Create the controller**

Create `app/Http/Controllers/JadwalController.php`:

```php
<?php

namespace App\Http\Controllers;

use App\Models\JadwalRuangan;
use App\Models\Kelas;
use Carbon\Carbon;

class JadwalController extends Controller
{
    public function index()
    {
        $nowDay = Carbon::now('Asia/Jakarta')->locale('id')->isoFormat('dddd');

        $rows = JadwalRuangan::with(['mata_kuliah', 'kelas'])
            ->where('hari', $nowDay)
            ->orderBy('jam_masuk')
            ->get();

        $timeRows = [];
        $rooms    = collect();
        $cells    = [];
        $currentSlot = $this->currentSlot();

        if ($rows->isNotEmpty()) {
            $minStart = $rows->min('jam_masuk');
            $maxEnd   = $rows->max('jam_keluar');

            $cursor = $minStart->copy();
            while ($cursor->lt($maxEnd)) {
                $timeRows[] = $cursor->format('H:i');
                $cursor->addMinutes(50);
            }

            $rooms = Kelas::whereIn('id', $rows->pluck('kelas_id')->unique())
                ->orderBy('kode_ruangan')
                ->get();

            foreach ($rows as $r) {
                $start = $r->jam_masuk->format('H:i');
                $span  = (int) round(
                    ($r->jam_keluar->timestamp - $r->jam_masuk->timestamp) / (50 * 60)
                );
                $bucket = &$cells[$r->kelas_id][$start][$r->dosen];
                $bucket ??= ['rowspan' => $span, 'entries' => collect()];
                $bucket['entries']->push($r);
            }
            unset($bucket);
        }

        return view('jadwal-hari-ini', compact(
            'timeRows', 'rooms', 'cells', 'nowDay', 'currentSlot'
        ));
    }

    private function currentSlot(): string
    {
        $now = Carbon::now('Asia/Jakarta');

        return sprintf('%02d:%02d', $now->hour, $now->minute < 30 ? 0 : 30);
    }
}
```

- [ ] **Step 6: Create the minimal Blade view**

Create `resources/views/jadwal-hari-ini.blade.php`:

```blade
<x-guest-layout>
    <meta http-equiv="refresh" content="60">

    <div class="bg-white rounded-xl p-4 shadow-sm ring-1 ring-gray-950/5">
        <div class="flex flex-col gap-1 text-center mb-4">
            <h2 class="text-sm font-semibold">Jadwal Perkuliahan Hari Ini</h2>
            <p class="text-xs text-gray-500">{{ Carbon\Carbon::now()->locale('id')->isoFormat('dddd, D MMMM Y') }}</p>
            <p class="text-[11px] text-gray-400">Halaman akan dimuat ulang otomatis setiap 60 detik.</p>
        </div>
    </div>
</x-guest-layout>
```

- [ ] **Step 7: Run the test to confirm it passes**

Run: `php artisan test --filter=JadwalHariIniTest::test_page_returns_200_and_shows_title`
Expected: PASS (1 passed)

- [ ] **Step 8: Commit**

```bash
git add phpunit.xml routes/web.php app/Http/Controllers/JadwalController.php resources/views/jadwal-hari-ini.blade.php tests/Feature/JadwalHariIniTest.php
git commit -m "feat(jadwal): add /jadwal-hari-ini route and minimal page"
```

---

### Task 2: Implement the data layer (time rows + room columns)

**Files:**
- Modify: `tests/Feature/JadwalHariIniTest.php` (add new test method)
- Modify: `app/Http/Controllers/JadwalController.php` (no change — the data layer was already added in Task 1)
- Modify: `resources/views/jadwal-hari-ini.blade.php` (extend the view to render the time rows and room headers)

**Interfaces:**
- Produces: For a seeded `Kelas` with `kode_ruangan = '3.01'` and a seeded `JadwalRuangan` today at `08:00–09:30` in that kelas, the page contains the time label `08.00 - 08.50` and the room header `3.01`.

- [ ] **Step 1: Add a new failing test for the data layer**

Add this method to `tests/Feature/JadwalHariIniTest.php` (inside the class):

```php
    public function test_data_layer_renders_time_rows_and_room_headers(): void
    {
        $kelas = \App\Models\Kelas::create([
            'id' => 'TEST01',
            'nama' => 'Ruang Kelas 3.01',
            'lantai' => 3,
            'kode_ruangan' => '3.01',
        ]);

        $mk = \App\Models\MataKuliah::create([
            'kode_mk' => 'IF101',
            'nama'    => 'Algoritma',
            'prodi'   => 'IF',
            'sks'     => 2,
        ]);

        \App\Models\JadwalRuangan::create([
            'hari'           => \Carbon\Carbon::now('Asia/Jakarta')->locale('id')->isoFormat('dddd'),
            'jam_masuk'      => '08:00',
            'jam_keluar'     => '09:30',
            'dosen'          => 'Pak Budi',
            'mata_kuliah_id' => $mk->id,
            'kelas_id'       => $kelas->id,
            'kelas'          => '2A',
        ]);

        $response = $this->get('/jadwal-hari-ini');

        $response->assertStatus(200);
        $response->assertSee('08.00 - 08.50');
        $response->assertSee('3.01');
        $response->assertSee('Ruang Kelas 3.01');
    }
```

- [ ] **Step 2: Run the new test to confirm it fails**

Run: `php artisan test --filter=JadwalHariIniTest::test_data_layer_renders_time_rows_and_room_headers`
Expected: FAIL (page body does not contain "08.00 - 08.50" or "3.01")

- [ ] **Step 3: Extend the Blade view to render the table shell**

Replace the contents of `resources/views/jadwal-hari-ini.blade.php` (everything between `<x-guest-layout>` and `</x-guest-layout>`):

```blade
<x-guest-layout>
    <meta http-equiv="refresh" content="60">

    <div class="bg-white rounded-xl p-4 shadow-sm ring-1 ring-gray-950/5">
        <div class="flex flex-col gap-1 text-center mb-4">
            <h2 class="text-sm font-semibold">Jadwal Perkuliahan Hari Ini</h2>
            <p class="text-xs text-gray-500">{{ Carbon\Carbon::now()->locale('id')->isoFormat('dddd, D MMMM Y') }}</p>
            <p class="text-[11px] text-gray-400">Halaman akan dimuat ulang otomatis setiap 60 detik.</p>
        </div>

        @if (empty($timeRows))
            <p class="text-center text-sm text-gray-500 py-8">Tidak ada jadwal hari ini.</p>
        @else
            <div class="overflow-x-auto">
                <table class="w-full text-xs border-collapse">
                    <thead>
                        <tr>
                            <th class="border border-gray-200 px-2 py-2 bg-gray-50 text-center">JAM</th>
                            @foreach ($rooms as $room)
                                <th class="border border-gray-200 px-2 py-2 bg-gray-50 text-center">
                                    <div class="font-semibold">{{ $room->kode_ruangan }}</div>
                                    <div class="text-[10px] text-gray-500">{{ $room->nama }}</div>
                                </th>
                            @endforeach
                        </tr>
                    </thead>
                    <tbody>
                        @php $skip = []; @endphp
                        @foreach ($timeRows as $slot)
                            @php
                                $slotEnd = \Carbon\Carbon::createFromFormat('H:i', $slot)->addMinutes(50)->format('H:i');
                                $isCurrent = $slot === $currentSlot;
                            @endphp
                            <tr @class(['!bg-green/15' => $isCurrent])>
                                <td class="border border-gray-200 px-2 py-2 font-semibold whitespace-nowrap">
                                    {{ str_replace(':', '.', $slot) }} - {{ str_replace(':', '.', $slotEnd) }}
                                </td>
                                @foreach ($rooms as $room)
                                    @if (($skip[$room->id] ?? 0) > 0)
                                        @php $skip[$room->id]--; @endphp
                                    @elseif (isset($cells[$room->id][$slot]))
                                        @php
                                            $byDosen   = $cells[$room->id][$slot];
                                            $firstDosen = array_key_first($byDosen);
                                            $cell      = $byDosen[$firstDosen];
                                            $skip[$room->id] = $cell['rowspan'] - 1;
                                        @endphp
                                        <td rowspan="{{ $cell['rowspan'] }}" class="border border-gray-200 px-2 py-2 align-top">
                                            @foreach ($cell['entries'] as $entry)
                                                <div>{{ $entry->kelas }} ({{ $entry->dosen }}) {{ optional($entry->mata_kuliah)->nama ?? '-' }}</div>
                                            @endforeach
                                        </td>
                                    @else
                                        <td class="border border-gray-200 px-2 py-2 text-center"><span class="text-gray-400">—</span></td>
                                    @endif
                                @endforeach
                            </tr>
                        @endforeach
                    </tbody>
                </table>
            </div>
        @endif
    </div>
</x-guest-layout>
```

- [ ] **Step 4: Run the data-layer test to confirm it passes**

Run: `php artisan test --filter=JadwalHariIniTest::test_data_layer_renders_time_rows_and_room_headers`
Expected: PASS (2 passed total)

- [ ] **Step 5: Run the full test suite to confirm nothing else broke**

Run: `php artisan test`
Expected: All tests pass (2 from this file, plus existing tests if any)

- [ ] **Step 6: Commit**

```bash
git add tests/Feature/JadwalHariIniTest.php resources/views/jadwal-hari-ini.blade.php
git commit -m "feat(jadwal): render time rows, room headers, cells with rowspan"
```

---

### Task 3: Add the "current-slot highlight" + "multi-entry cell" + "empty-day fallback" tests

**Files:**
- Modify: `tests/Feature/JadwalHariIniTest.php` (add three new test methods)
- Modify: `app/Http/Controllers/JadwalController.php` (no controller change needed — `setTestNow` is used in tests; empty-day is already in the Blade)
- Modify: `resources/views/jadwal-hari-ini.blade.php` (no view change needed — current-slot class is already in the Blade)

**Interfaces:**
- Produces: Test for current-slot highlight (uses `Carbon::setTestNow()`), multi-entry cell (two `jadwal_ruangan` rows share `kelas_id + jam_masuk + jam_keluar + dosen`), and empty-day fallback (no rows at all).

- [ ] **Step 1: Add three new failing tests**

Add these methods to `tests/Feature/JadwalHariIniTest.php` (inside the class):

```php
    public function test_current_slot_row_is_highlighted(): void
    {
        \Carbon\Carbon::setTestNow(\Carbon\Carbon::parse('2026-07-29 08:15:00', 'Asia/Jakarta'));

        $kelas = \App\Models\Kelas::create([
            'id' => 'TEST02',
            'nama' => 'Ruang Kelas 3.02',
            'lantai' => 3,
            'kode_ruangan' => '3.02',
        ]);

        $mk = \App\Models\MataKuliah::create([
            'kode_mk' => 'IF102',
            'nama'    => 'Basis Data',
            'prodi'   => 'IF',
            'sks'     => 2,
        ]);

        \App\Models\JadwalRuangan::create([
            'hari'           => \Carbon\Carbon::now('Asia/Jakarta')->locale('id')->isoFormat('dddd'),
            'jam_masuk'      => '08:00',
            'jam_keluar'     => '09:30',
            'dosen'          => 'Pak Andi',
            'mata_kuliah_id' => $mk->id,
            'kelas_id'       => $kelas->id,
            'kelas'          => '2B',
        ]);

        $response = $this->get('/jadwal-hari-ini');

        $response->assertStatus(200);
        $response->assertSee('!bg-green/15', false);

        \Carbon\Carbon::setTestNow();
    }

    public function test_multiple_entries_in_one_cell_are_stacked(): void
    {
        $kelas = \App\Models\Kelas::create([
            'id' => 'TEST03',
            'nama' => 'Ruang Kelas 3.03',
            'lantai' => 3,
            'kode_ruangan' => '3.03',
        ]);

        $mk1 = \App\Models\MataKuliah::create(['kode_mk' => 'IF201', 'nama' => 'Matematika Diskrit', 'prodi' => 'IF', 'sks' => 2]);
        $mk2 = \App\Models\MataKuliah::create(['kode_mk' => 'IF202', 'nama' => 'Aljabar Linear',     'prodi' => 'IF', 'sks' => 2]);

        $hari = \Carbon\Carbon::now('Asia/Jakarta')->locale('id')->isoFormat('dddd');

        // Same room, same time, same dosen, different kelas group -> 2 entries in one cell
        \App\Models\JadwalRuangan::create([
            'hari' => $hari, 'jam_masuk' => '10:00', 'jam_keluar' => '11:30',
            'dosen' => 'Ivan Ridwan', 'mata_kuliah_id' => $mk1->id,
            'kelas_id' => $kelas->id, 'kelas' => '2a',
        ]);
        \App\Models\JadwalRuangan::create([
            'hari' => $hari, 'jam_masuk' => '10:00', 'jam_keluar' => '11:30',
            'dosen' => 'Ivan Ridwan', 'mata_kuliah_id' => $mk2->id,
            'kelas_id' => $kelas->id, 'kelas' => '2c',
        ]);

        $response = $this->get('/jadwal-hari-ini');

        $response->assertStatus(200);
        $response->assertSee('2a (Ivan Ridwan) Matematika Diskrit');
        $response->assertSee('2c (Ivan Ridwan) Aljabar Linear');
        // Single rowspan'd cell
        $response->assertSee('rowspan="2"', false);
    }

    public function test_empty_day_renders_fallback_message(): void
    {
        $response = $this->get('/jadwal-hari-ini');

        $response->assertStatus(200);
        $response->assertSee('Tidak ada jadwal hari ini.');
    }
```

- [ ] **Step 2: Run the new tests**

Run: `php artisan test --filter=JadwalHariIniTest`
Expected: All 5 tests pass (the controller and view from Tasks 1 and 2 already support these scenarios).

- [ ] **Step 3: If any test fails, fix the controller or view**

Most likely failure points:
- **Empty-day test** fails because the controller passes `[]` for `timeRows` and the Blade falls into the `else` branch — should already work.
- **Multi-entry test** fails if `RefreshDatabase` is rolling back and the second `JadwalRuangan::create` collides — they have different `kelas` field so should be fine.
- **Current-slot test** fails if `setTestNow` doesn't apply because the controller uses `Carbon::now('Asia/Jakarta')` — `setTestNow` does affect `Carbon::now()` so this should work.

If the multi-entry test fails because the two entries land in different time slots due to floating-point time math, re-check the `span` calculation in the controller. The expected behavior is: two rows with identical `jam_masuk=10:00` and `jam_keluar=11:30` should produce one bucket with `rowspan=2` and `entries` of size 2.

- [ ] **Step 4: Commit**

```bash
git add tests/Feature/JadwalHariIniTest.php
git commit -m "test(jadwal): cover current-slot highlight, multi-entry cells, empty day"
```

---

### Task 4: Add the "Lihat Jadwal Hari Ini" link from `app/index.blade.php`

**Files:**
- Modify: `tests/Feature/JadwalHariIniTest.php` (add a test for the link)
- Modify: `resources/views/app/index.blade.php` (add the link in the header)

**Interfaces:**
- Produces: `GET /` (kontrol page) renders a link with text "Lihat Jadwal Hari Ini" pointing to `route('jadwal.hari-ini')`.

- [ ] **Step 1: Add a failing test for the link**

Add this method to `tests/Feature/JadwalHariIniTest.php` (inside the class):

```php
    public function test_kontrol_page_links_to_jadwal_hari_ini(): void
    {
        $response = $this->get('/');

        $response->assertStatus(200);
        $response->assertSee('Lihat Jadwal Hari Ini');
        $response->assertSee(route('jadwal.hari-ini'), false);
    }
```

- [ ] **Step 2: Run the test to confirm it fails**

Run: `php artisan test --filter=JadwalHariIniTest::test_kontrol_page_links_to_jadwal_hari_ini`
Expected: FAIL — body does not contain "Lihat Jadwal Hari Ini"

- [ ] **Step 3: Add the link to `resources/views/app/index.blade.php`**

In `resources/views/app/index.blade.php`, find the `<nav class="mb-0 md:mb-2">` block (around line 4) and replace it with:

```blade
        <nav class="mb-0 md:mb-2 flex flex-wrap items-center justify-between gap-2">
            <ol class="flex flex-wrap items-center gap-x-2">
                <li class="flex items-center gap-x-2">
                    <a href="{{ route('kontrol.index') }}" class="text-xs lg:text-sm font-medium text-gray-500 hover:underline dark:text-gray-400 transition duration-75 hover:text-gray-700 dark:hover:text-gray-200">
                        Daftar Ruangan FASILKOM
                    </a>
                </li>
                <li class="flex items-center gap-x-2">
                    <svg class="flex h-5 w-5 text-gray-400 dark:text-gray-500 rtl:hidden" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" data-slot="icon">
                        <path fill-rule="evenodd" d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z" fill-rule="evenodd"></path>
                    </svg>
                    <span class="text-xs lg:text-sm font-medium text-gray-500 dark:text-gray-400">
                        List
                    </span>
                </li>
            </ol>
            <a href="{{ route('jadwal.hari-ini') }}" class="inline-flex items-center gap-1 text-xs lg:text-sm font-semibold text-primary-600 hover:text-primary-700 dark:text-primary-400">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4">
                    <path fill-rule="evenodd" d="M6.75 2.25A.75.75 0 0 1 7.5 3v1.5h9V3A.75.75 0 0 1 18 3v1.5h.75a3 3 0 0 1 3 3v11.25a3 3 0 0 1-3 3H5.25a3 3 0 0 1-3-3V7.5a3 3 0 0 1 3-3H6V3a.75.75 0 0 1 .75-.75Zm13.5 9a1.5 1.5 0 0 0-1.5-1.5H5.25a1.5 1.5 0 0 0-1.5 1.5v7.5a1.5 1.5 0 0 0 1.5 1.5h13.5a1.5 1.5 0 0 0 1.5-1.5v-7.5Z" clip-rule="evenodd" />
                </svg>
                Lihat Jadwal Hari Ini
            </a>
        </nav>
```

- [ ] **Step 4: Run the test to confirm it passes**

Run: `php artisan test --filter=JadwalHariIniTest::test_kontrol_page_links_to_jadwal_hari_ini`
Expected: PASS

- [ ] **Step 5: Run the full test suite**

Run: `php artisan test`
Expected: All tests pass (6 from `JadwalHariIniTest`, plus existing tests)

- [ ] **Step 6: Commit**

```bash
git add tests/Feature/JadwalHariIniTest.php resources/views/app/index.blade.php
git commit -m "feat(jadwal): link 'Lihat Jadwal Hari Ini' from kontrol page"
```

---

### Task 5: Manual browser verification

**Files:** none (verification only)

- [ ] **Step 1: Start the dev server**

Run: `php artisan serve`
Expected: server starts on `http://127.0.0.1:8000`

- [ ] **Step 2: Open the kontrol page and verify the link**

Open: `http://127.0.0.1:8000/`
Expected: Page loads, "Lihat Jadwal Hari Ini" link is visible in the header area.

- [ ] **Step 3: Click the link and verify the new page**

Click the link or open `http://127.0.0.1:8000/jadwal-hari-ini` directly.
Expected:
- Page title "Jadwal Perkuliahan Hari Ini" is visible
- Date subtitle shows today's date in Bahasa (e.g. "Rabu, 29 Juli 2026")
- Note "Halaman akan dimuat ulang otomatis setiap 60 detik." is visible
- A `<table>` is rendered with:
  - "JAM" column header on the left
  - Room columns on the right with `kode_ruangan` on top and `nama` below
  - Time row labels in `HH.MM - HH.MM` format
  - Cells with `{kelas} ({dosen}) {mata_kuliah_nama}` content
  - Empty cells showing "—"
  - If any class is currently in progress, that row is highlighted green
- Multi-SKS classes span multiple rows via `rowspan`
- A class with the same `(room, time, dosen)` and multiple `kelas` values renders stacked

- [ ] **Step 4: Verify the auto-refresh**

Wait 60 seconds (or temporarily change the meta to `content="5"` to test faster, then revert).
Expected: Page reloads on its own after the interval.

- [ ] **Step 5: No commit needed**

This task is verification only. If issues are found, fix them and amend or add a follow-up commit.

---

## Self-Review

**1. Spec coverage:**
- "Rows = time slots (50-min)" — Task 1 controller builds `timeRows` in 50-min steps, Task 2 renders them. ✓
- "Columns = rooms" — Task 1 controller filters `Kelas::whereIn('id', $rows->pluck('kelas_id')->unique())->orderBy('kode_ruangan')`, Task 2 renders them. ✓
- "Cell content `{kelas} ({dosen}) {mata_kuliah_nama}`" — Task 2 Blade render. ✓
- "Multi-entry cells when grouped by (kelas_id, jam_masuk, jam_keluar, dosen)" — Task 1 controller cell map, Task 2 Blade, Task 3 test. ✓
- "Empty day fallback" — Task 1 controller passes empty arrays, Task 2 Blade `if (empty($timeRows))`, Task 3 test. ✓
- "Current-slot highlight" — Task 1 controller `currentSlot()`, Task 2 Blade `!bg-green/15`, Task 3 test. ✓
- "Auto-refresh 60s" — Task 1 view `<meta http-equiv="refresh" content="60">`. ✓
- "Standalone route, no room param" — Task 1 `GET /jadwal-hari-ini` no `{id}`. ✓
- "Link from kontrol page" — Task 4. ✓
- "50-min slots from min(jam_masuk) to max(jam_keluar)" — Task 1 controller `addMinutes(50)`. ✓
- "Rowspan derived from jam_keluar - jam_masuk / 50min" — Task 1 controller `span` calc. ✓
- "kode_ruangan (bold) on top, nama (subtitle)" — Task 2 Blade thead. ✓
- "HH.MM - HH.MM time labels" — Task 2 Blade `str_replace(':', '.', ...)`. ✓
- "Empty cell = —" — Task 2 Blade `<span class="text-gray-400">—</span>`. ✓

**2. Placeholder scan:** No TBDs, no TODOs, no "implement later" steps. Every code step has the full code block.

**3. Type/name consistency:**
- `currentSlot` is a private method on the controller, called in `index()`, passed to the view as `currentSlot`, used in Blade as `$currentSlot`. ✓
- Cell-map shape `[$kelas_id][$jam_masuk H:i][$dosen] => ['rowspan' => int, 'entries' => Collection]` is consistent between the controller (Task 1) and Blade access (Task 2). ✓
- `$skip[$room->id]` counter is decremented in the Blade and reset per row (the `@php $skip = []; @endphp` is at the top of the `<tbody>`, scoped per row). ✓
- `Route::get('/jadwal-hari-ini', [JadwalController::class, 'index'])->name('jadwal.hari-ini')` — controller, action, route name all match. ✓
