# Design: Dosen typeahead picker for Jadwal

Date: 2026-08-03

## Problem

The `dosen` field in the Jadwal add/edit forms is a free-text input. The user wants a
typeahead that matches against `data_personil` records where `jabatan = dosen`, and
requires selection from that list (strict match). The jadwal row must store both the
display name and a reference to the personil record.

## Decisions

- `jadwal_ruangan.dosen_id` becomes a **NOT NULL** FK to `data_personil.id`
  (onDelete `RESTRICT` — a dosen in use cannot be deleted).
- Existing jadwal rows are backfilled by exact `nama` match against personil dosen.
  If any row cannot be matched, the migration **throws with a clear message listing
  the offending rows** so the user fixes them manually (no auto-create, no delete).
- `dosen` column keeps storing the personil `nama` (display text). `dosen_id` is the
  reference. Server derives the `nama` from the personil record — client text is not
  trusted.
- Typeahead fetches from the public API on type (debounced), matching is strict —
  free text that doesn't match is rejected with inline error text, and the submit
  button is disabled until all required inputs are filled and the dosen is valid.

## Changes

### 1. Data layer

- Migration `add_dosen_id_to_jadwal_ruangan_table`:
  - add nullable `dosen_id` (unsignedBigInteger, FK -> data_personil.id, RESTRICT)
  - backfill: match each jadwal's `dosen` text to personil (`jabatan = dosen`,
    exact `nama`); throw listing offenders if unmatched
  - then alter to NOT NULL
- `JadwalRuangan` model: add `dosen_id` to `$fillable`, add `dosenPersonil()`
  belongsTo relation.

### 2. API

- Extend public `GET /api/data-personil` with optional query params:
  - `jabatan=dosen` -> filter to dosen only
  - `search=...` -> case-insensitive `nama LIKE` filter

### 3. Frontend

- New Alpine component `resources/js/alpine/components/dosen-picker.js` registered
  as `Alpine.data('dosenPicker', ...)`:
  - debounced (300ms) fetch from `/api/data-personil?jabatan=dosen&search=...`
  - dropdown list under input; click / keyboard (ArrowUp/Down, Enter, Escape)
  - hidden `dosen_id` input set on selection; visible input shows `nama`
  - error text under input when typed text has no match
  - `canSubmit` flag used to disable the submit button until all required inputs
    are filled and dosen is a valid match
- Apply in both forms:
  - Add: `resources/views/components/form-data-create.blade.php` (jadwal template)
  - Edit: `resources/views/components/datatable/jadwal-action-modal.blade.php`
    (prefill from `dosen_id`; `jadwal-action-btn.blade.php` passes `dosen_id`)

### 4. Server validation & persistence

- `DataController@storeJadwal` / `@jadwalUpdate`:
  - `dosen_id` required|exists:data_personil,id
  - verify the personil `jabatan = dosen`
  - store personil `nama` as `dosen` + `dosen_id`

### 5. Tests

- `JadwalAdminCrudTest`: create a personil dosen in `setUp`; send `dosen_id`; add
  tests for missing `dosen_id` (422), tendik id (422), and that `dosen` stores the
  personil `nama` not the posted text.
- `JadwalHariIniTest`: rows it creates directly must include a valid `dosen_id`.
- `DataPersonilApiTest`: tests for `jabatan` and `search` filters.

## Non-goals

- No new personil CRUD in this work.
- Public pages (jadwal list, jadwal-hari-ini) display only `dosen` (name); no change.
