# Journals Admin CRUD — Plan & Build Record

> **Status (2026-05-17):** ✅ Implemented. Retrospective plan + reference.
>
> **Goal:** Give the Journals content type the same admin CRUD as the other modules, using its M:N sections taxonomy and three rich-text fields. Wire the previously-dead "Journals" sidebar link. **Last remaining dead Content sidebar link is now wired.**

---

## 1. Terminology

This is the only content type where the **admin label differs from the public label**:

| Surface | Label |
|---|---|
| DB table | `journals` |
| Eloquent model | `App\Models\Journal` |
| Admin sidebar / URL / page titles | "Journals" — `/admin/journals` |
| **Public site (user-facing)** | **"Judgements"** — `/user/judgements`, `UserController@judgements` |

The public-facing terminology was already in place. The admin module keeps "Journals" to match the sidebar that existed before this work. Anyone reading code that says `$judgement = Journal::with('sections.act')->findOrFail($id)` in `UserController` now has documented context.

---

## 2. Schema

```sql
journals
├── id
├── note_no               INT NULL                  -- serial number
├── publication_year      INT NULL                  -- year (1900–2099)
├── party_name            VARCHAR(250) NULL
├── volumn                VARCHAR(50)  NULL         -- ⚠ misspelled in DB; kept as-is
├── head_note             MEDIUMTEXT  NULL          -- rich text (main body)
├── head_note_strip_tag   LONGTEXT    NULL          -- FULLTEXT-search shadow
├── citation              MEDIUMTEXT  NULL          -- rich text
├── citation_strip_tag    LONGTEXT    NULL
├── held                  MEDIUMTEXT  NULL          -- rich text
├── held_strip_tag        LONGTEXT    NULL
└── created_at, updated_at  TIMESTAMP  DB-defaulted

journal_sections                                    -- M:N pivot
├── id, journal_id, section_id
└── created_at, updated_at  TIMESTAMP  DB-defaulted
```

Schema quirks:

| Quirk | Note |
|---|---|
| `volumn` typo | Kept as-is to avoid breaking the public side. |
| Three rich-text fields | `citation` (short), `head_note` (main body, tall), `held` (medium). Each has a paired `*_strip_tag` column. |
| Public search index | `MATCH(head_note_strip_tag, held_strip_tag, citation_strip_tag) AGAINST(...)` — all three strip-tag fields **must** stay populated on every save. |
| Live counts (2026-05-17) | 600 acts, 5,479 sections, ~42K journals, ~32K pivot rows. Mean ~0.76 sections per journal. |
| `$timestamps = false` on model | Both `created_at` and `updated_at` have DB-side `current_timestamp()` defaults — Eloquent doesn't need to touch them. Same pattern as Circulars/Amendments. |

---

## 3. Architecture Decisions

| Decision | Choice | Reason |
|---|---|---|
| CRUD shape | Page-based, six methods + one AJAX helper (`sectionsByAct`) | Matches the rest of the admin. The AJAX helper is the only extra surface area. |
| Sections picker UX | **Stacked rows, three columns each: Act · Sections (trigger+panel like the frontend) · Remove** | Each row's section picker uses the same dropdown-trigger / selected-tags / checkbox-panel pattern as the public-side `users/judgements*.blade.php` views (CSS adapted from `public/theme/front/assets/css/main.css` §"Custom Section Selector Design"). Clicking the trigger toggles a panel with a search input + scrollable checkboxes; checkboxes are named `section_ids[]` so the browser collects them across all rows on submit. `$journal->sections()->sync(...)` dedupes server-side. "+ Add Act" button appends a fresh row. Initial render on edit: one row per Act in the attachment set. Act dropdown is enhanced with **Choices.js** (the admin theme's bundled autocomplete; same lib used by Articles/Samasyas — no extra CDN). |
| Sections persistence | `$journal->sections()->sync($section_ids)` | Idempotent; handles add/remove diff in one call. Explicit `->detach()` on delete because the pivot has no FK cascade. |
| Three CKEditor instances | One `<textarea>` each, initialised via a shared `initCk()` helper | Toolbar, image controls, and table contentToolbar all lifted verbatim from `admin/articles/partials/form_scripts.blade.php` — fonts, colours, image upload, page break, etc. all available. All three editors share a single CSS rule `.ck-editor__editable_inline { min-height: 500px; }`, matching the Articles form. (Earlier iteration used a trimmed toolbar and per-editor heights `ck-short`/`ck-medium`/`ck-tall`; replaced 2026-05-17 so the editing experience matches Articles 1:1.) |
| `*_strip_tag` recomputation | Always recomputed on save via `strip_tags()` + whitespace collapse | Critical — public proximity search depends on all three indexes. |
| Validation | Inline `Validator::make`, with `section_ids` as `required\|array\|min:1` plus per-element `exists:sections,id` | Project convention; explicit `min:1` since the empty-array case is meaningful here. |
| Sections picker — no `innerHTML` | All DOM mutations use `replaceChildren()` + `document.createElement()` + `textContent`/`appendChild` | Defensive: section labels come from the DB; even though they're admin-curated, avoiding `innerHTML` removes the XSS surface entirely. (Flagged by the project's security hook on the first pass.) |

**Explicitly NOT included** (minimal-scope rule):

- Public-side `judgements` listing / search / export — untouched
- Fixing the `volumn` → `volume` typo (would touch `UserController`, exports, search forms)
- Bulk import/export of journals
- An admin module for the `journal_sections` pivot directly (managed inline via the picker)
- Renaming "Journals" to "Judgements" in the admin sidebar — admin terminology kept stable

---

## 4. Validation Rules

```php
'id'               => 'nullable|integer',
'note_no'          => 'nullable|integer',
'publication_year' => 'required|integer|min:1900|max:2099',
'party_name'       => 'required|string|max:250',
'volumn'           => 'nullable|string|max:50',
'citation'         => 'required|string|max:5000000',
'head_note'        => 'required|string|max:5000000',
'held'             => 'required|string|max:5000000',
'section_ids'      => 'required|array|min:1',
'section_ids.*'    => 'integer|exists:sections,id',
```

Attribute aliases: `publication_year` → "publication year", `party_name` → "party name", `volumn` → "volume", `head_note` → "head note", `section_ids` → "sections".

---

## 5. Files

### Added

| Path | Purpose |
|---|---|
| `app/Http/Controllers/Admin/JournalController.php` | Six CRUD methods + `sectionsByAct()` AJAX endpoint + `actOptions()` + `stripTag()` helpers. |
| `resources/views/admin/journals/index.blade.php` | DataTable list (with a note that public users see this as "Judgements"). |
| `resources/views/admin/journals/create.blade.php` / `edit.blade.php` | Page wrappers — load CKEditor 5 only (no Flatpickr — there's no date input). |
| `resources/views/admin/journals/partials/form.blade.php` | Shared form (party/year/note#/volume, sections picker, citation, head_note, held). |
| `resources/views/admin/journals/partials/form_scripts.blade.php` | Three CKEditor instances via shared `initCk()`, Choices.js on Act picker, AJAX section loader, chip-list bookkeeping (no `innerHTML`). |

### Modified

| Path | Change |
|---|---|
| `app/Models/Journal.php` | Added `$fillable` (10 columns including three `*_strip_tag` columns) + `$casts` (`note_no`, `publication_year` → int). Kept `$timestamps = false` (DB defaults). Kept existing `sections()` belongsToMany. |
| `routes/web.php` | Imported `AdminJournalController`; added `admin.journals.*` route group inside `auth + role:super-admin` middleware, including the new `sections-by-act` POST endpoint. |
| `resources/views/admin/partials/sidebar.blade.php` | Added `$isJournals`; included in `$contentOpen`; wired the dead `#` link. |

### Routes registered

```
GET|HEAD   admin/journals                          admin.journals.index
GET|HEAD   admin/journals/create                   admin.journals.create
POST       admin/journals/data_table               admin.journals.data_table
POST       admin/journals/remove                   admin.journals.remove
POST       admin/journals/save                     admin.journals.save
POST       admin/journals/sections-by-act          admin.journals.sections-by-act
GET|HEAD   admin/journals/{journal}/edit           admin.journals.edit
```

---

## 6. Form Layout

1. **Row 1:** Party Name (5 cols, required, max 250) · Year (2 cols, required, 1900–2099) · Note # (2 cols, optional) · Volume (3 cols, optional, max 50, DB column = `volumn`)
2. **Row 2:** Sections picker — stacked rows, each laid out as three columns: **Act (5 cols)** · **Sections trigger+panel (6 cols)** · **Remove (1 col)**. Click the Sections trigger to open a dropdown panel containing a search box + scrollable checkbox list (visually matches the public-side `users/judgements*` pages). Selected sections appear as tags inside the trigger; the panel auto-closes on outside-click. The "+ Add Act" button below the rows spawns a fresh empty row. On edit, one row is pre-rendered per attached Act with the right checkboxes (and tags) pre-set.
3. **Row 3:** Citation (CKEditor 5 Classic, min-height 500px — Articles-style toolbar)
4. **Row 4:** Head Note (CKEditor 5 Classic, min-height 500px — main body, same toolbar)
5. **Row 5:** Held (CKEditor 5 Classic, min-height 500px — same toolbar)
6. **Footer:** "Back to list" · "Save Journal" / "Update Journal"

DataTable columns:

`S.No. | Note # | Year | Party Name | Volume | Sections (count) | Action`

Searchable across `party_name`, `head_note_strip_tag`, `held_strip_tag`, `citation_strip_tag`. Default sort `id DESC`. Sections column is a count (via `withCount('sections')`).

---

## 7. Verification

| Check | Result |
|---|---|
| `php -l` on `JournalController.php`, `Journal.php`, `routes/web.php` | No syntax errors. |
| `php artisan route:list --name=admin.journals` | 7 routes registered (6 CRUD + `sections-by-act`). |
| Blade compile + render harness | All views compile. (`$errors`-harness warning expected — same as the other modules.) |

**Not verified — needs browser session as `mpsjajournal@gmail.com`:**

- End-to-end create / edit / delete through admin UI
- DataTable AJAX payload on populated rows
- Sections picker round-trip: pick Act → AJAX loads sections → pick + Add → chips appear → remove a chip → save → re-edit shows expected attached sections
- Three CKEditor instances initialise without errors and submit their content correctly
- Existing journals open and edit cleanly (note: existing rows with NULL `publication_year` / `party_name` will fail validation on edit until populated)
- Delete also removes pivot rows (the controller calls `$journal->sections()->detach()` before delete)

---

## 8. Known caveats

- **`volumn` field**: the input name is `volumn` (matching the DB), but the UI label is "Volume". A future cleanup pass could rename the column and update the public site in one migration — out of scope here.
- **Existing data gaps**: Like the Articles author/publish_date change, any existing journal with NULL `publication_year` or `party_name` will fail validation on edit until those fields are filled in. Forward-only safety, no data migration needed.
- **No sort on Sections-count column**: The DataTable column is rendered server-side as `withCount('sections')` but the `$columns[5]` slot is `null` (unsortable). Sorting by count would need a subquery; not worth the complexity for what is essentially a glanceable column.

---

## 9. Follow-ups (not done)

- Renaming `volumn` → `volume` (DB + public site) — separate, isolated cleanup ticket.
- Renaming the public-side "Judgements" terminology to "Journals" (or vice versa) — UX decision the user should make first, then code change.
- Same no-tests rationale as previous plans.
- **All Content sidebar links are now wired.** The remaining dead links are under "Access" (Roles) — separate work area.
