# `detail.blade.php` Reply Form — Track & Plan in Better Way

**Date:** 2026-07-08
**Scope:** `resources/views/app/berita/partials/detail.blade.php` and a small Alpine state companion. No backend changes, no model changes, no other Blade files.

## Problem

The "Balas Laporan" reply form has three working-but-broken behaviors:

1. The submit button at the bottom of the modal is rendered **outside** the `<x-crud.form>` element. Clicking it does not submit the form.
2. `x-bind:disabled="loading"` on that button references the form's `crudForm.loading`, but the button is outside the form's Alpine scope, so the binding is dead.
3. The hidden `status` input can be empty. Backend requires it, but the UI gives no signal that a status must be picked, and no signal that "Dijawab" needs a textarea, or "Diselesaikan" needs an image.

The form lives inside `@if($berita->status === 'Proses')`, so it cannot wrap the modal's content. The button must stay at the bottom of the modal, outside the form, but must still submit it and reflect its loading state.

## Goal

- Make the submit button work.
- Track what the admin is doing (status, required-field completion).
- Plan what will happen — show a one-line summary of the action + the remaining requirement, if any.
- Disable submit until the form is valid; prevent double-submit during the request.
- Preserve the existing UI/UX exactly (same colors, spacing, copy, layout). Only addition is one small status line.

## Approach (chosen): HTML5 `form=` attribute + shared Alpine wrapper

A parent `<div x-data="replyForm()">` owns the validation/UX state. The form keeps its existing `crudForm` for HTTP transport. The button uses HTML5 `form="berita-reply-form"` to submit the form across siblings. A small `crud-loading` window event bridges `loading` from `crudForm` (inside the form) to the parent wrapper (so the button can disable while the request is in flight).

## Architecture

```
<div x-data="replyForm()">                 ← parent wrapper, single source of UX state
  │
  ├─ <p x-show="status" class="text-[11px] text-gray-500">Aksi: <span x-text="planSummary"></span></p>
  │
  ├─ <x-crud.form id="berita-reply-form">  ← unchanged except for id; owns HTTP via crudForm
  │   - status hidden input: x-model="status"     (reads/writes parent's status)
  │   - textarea: @input updates parent.hasAnswer
  │   - file input: @change updates parent.hasImage
  │   - dispatches `crud-loading` window event on each loading state flip
  │
  └─ <x-crud.button form="berita-reply-form"  x-bind:disabled="!isValid">
        Kirim / spinner
```

## State model — `replyForm()`

```js
export default () => ({
    status: '',          // '' | 'Dijawab' | 'Diselesaikan'
    hasAnswer: false,    // textarea has non-whitespace content
    hasImage: false,     // file input has a selection
    loading: false,      // mirrored from crudForm via window event

    get isValid() {
        if (this.loading) return false;
        if (this.status === 'Dijawab') return this.hasAnswer;
        if (this.status === 'Diselesaikan') return this.hasImage;
        return false;
    },

    get planSummary() {
        if (this.status === 'Dijawab') {
            return this.hasAnswer
                ? 'Balas dengan teks balasan.'
                : 'Tulis balasan terlebih dahulu.';
        }
        if (this.status === 'Diselesaikan') {
            return this.hasImage
                ? 'Selesaikan laporan dengan lampiran foto.'
                : 'Lampirkan foto balasan terlebih dahulu.';
        }
        return '';
    },

    init() {
        this._onLoading = (e) => { this.loading = !!e.detail?.loading; };
        window.addEventListener('crud-loading', this._onLoading);
    },

    destroy() {
        window.removeEventListener('crud-loading', this._onLoading);
    },
});
```

**Validation rules:**
- `status === ''` → invalid; no plan summary shown
- `status === 'Dijawab'` → valid only when `hasAnswer === true`
- `status === 'Diselesaikan'` → valid only when `hasImage === true`
- `loading === true` → button disabled (prevents double-submit) regardless of `isValid`

## File-by-file changes

| File | Type | Change |
|---|---|---|
| `resources/js/alpine/forms/reply-form.js` | **NEW** | `replyForm()` Alpine component above |
| `resources/js/app.js` | edit | Register `replyForm` next to the existing `crudForm` registration |
| `resources/js/alpine/forms/crud-form.js` | edit | Dispatch `crud-loading` window event at start and end of `submit()` |
| `resources/views/app/berita/partials/detail.blade.php` | edit | Restructure per architecture above. Add `x-data="replyForm()"` on panel, form `id=`, button `form=` + `x-bind:disabled=`, plan summary line, `@input` on textarea, `@change` on file input, remove inner `x-data="{ status: '' }"` |
| `docs/superpowers/specs/2026-07-08-berita-detail-reply-form-design.md` | **NEW** | This design doc |

## UI/UX preservation

- Right "Balas Laporan" panel: same background, padding, rounded, gap, fonts
- "Balas" / "Selesai" toggle buttons: same classes, same active-state colors
- Textarea: same classes
- File input + image preview block: identical
- Submit button: identical classes
- **Only addition**: one `<p class="text-[11px] text-gray-500">Aksi: ...</p>` line above the form content, visible only when a status is chosen

## Error handling

- **Client**: submit disabled while loading, `isValid` gate prevents invalid submissions; server errors surfaced via existing global toast
- **Server** (no change): `BeritaAdminController::update()` validates required+enum `status`, nullable `jawaban`, nullable `jawaban_image`; 500 on throw returned as 500 JSON
- **Edge case — modal open with no reply chosen**: button rendered, disabled, no plan summary, `cursor-not-allowed`

## Out of scope

- Inline field-level error display
- Refactoring `x-crud.form` component
- Changes to the laporan (left) panel
- Backend validation or model changes
- Any visual redesign
