# Research Articles Admin CRUD — Plan & Build Record

> **Status (2026-05-17):** ✅ Implemented. Retrospective plan + reference.
>
> **Goal:** Give the Research Articles content type the same admin CRUD as Articles/Circulars/Amendments/Samasyas, using its own (much simpler) schema, and wire the previously-dead "Research Articles" sidebar link.

---

## 1. Schema

`research_articles` table (from `2026_05_09_072738_create_research_articles_table.php`):

```php
$table->id();
$table->string('title');                                       // VARCHAR(255), NOT NULL
$table->longText('article')->nullable();                       // rich-text body
$table->longText('article_strip_tag')->nullable();             // FULLTEXT search column
$table->string('author')->nullable();                          // VARCHAR(255)
$table->timestamp('publish_date')->nullable();                 // ⚠ TIMESTAMP not DATE
$table->timestamps();                                          // created_at + updated_at
$table->fullText(['title','article_strip_tag','author'], 'ft_research_articles_keyword');
$table->fullText(['article_strip_tag'],                  'ft_research_articles_content');
```

Two things make this schema different from every other content table in the project:

| Aspect | Reality | Why it matters |
|---|---|---|
| Timestamps | Uses Laravel `timestamps()` (`created_at` + `updated_at`) | Model keeps `$timestamps = true` (default). Controller doesn't manually set `created_at`. Smaller `save()` method. |
| `publish_date` type | `TIMESTAMP` (not `DATE` like every other content table) | Date input still works — Flatpickr submits `Y-m-d`; MySQL stores midnight. Model casts as `datetime`. |
| Taxonomy | None | No category, no subcategory, no subject. Simplest form in the set. |
| Search index | Two FULLTEXT indexes (`ft_research_articles_keyword` covering `title + article_strip_tag + author`, `ft_research_articles_content` over `article_strip_tag`) | Public-side `MATCH(title, article_strip_tag, author) AGAINST(...)` uses the keyword index. `article_strip_tag` must stay populated on every save. |

---

## 2. Architecture Decisions

| Decision | Choice | Reason |
|---|---|---|
| CRUD shape | Page-based, six methods | Same as the other content modules. |
| Model `$fillable` vs `$guarded` | Switched from `protected $guarded = []` to explicit `$fillable` | Matches the pattern used by every other content model in this codebase; explicit `$fillable` is the safer default. |
| `$timestamps` | Kept as Laravel default (`true`) | `timestamps()` migration column → Eloquent manages both `created_at` and `updated_at`. No manual `created_at` set on insert. |
| `publish_date` cast | `datetime` | TIMESTAMP column requires datetime cast (DATE cast would silently truncate to date-only on read). |
| URL convention | `/admin/research-articles` | Matches existing hyphen-separated admin URLs (`article-categories`, `circular-subjects`). User-facing route is `user.research.articles` (dotted) — admin namespace stays internally consistent. |
| Route parameter | `{researchArticle}` (camelCase) | Laravel route-model-binding convention; matches what `Route::get('{article}/edit', …)` does in the Articles group. |
| `article_strip_tag` | Always recomputed on save via `strip_tags($article)` + whitespace collapse | Critical for `MATCH(title, article_strip_tag, author) AGAINST(...)` used by the public-side search. |
| Validation | Inline `Validator::make` in `save()` | Project convention. |

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

- Public-side `UserController@researchArticles` / `getResearchArticles` / exports — unchanged
- Adding a category/author taxonomy — schema has none, and the user-side search treats `author` as a plain LIKE field, not a lookup
- Journals (the last remaining dead Content sidebar link)
- Extracting a shared "content CRUD" trait — five controllers now with three different shapes (with/without taxonomy, with/without subcategory cascade, manual vs Laravel timestamps); the abstraction would have to carry too many parameters

---

## 3. Validation Rules

```php
'id'           => 'nullable|integer',
'title'        => 'required|string|max:255',
'author'       => 'required|string|max:255',
'publish_date' => 'required|date',
'article'      => 'required|string|max:5000000',
```

Attribute alias: `article` → "article body".

---

## 4. Files

### Added

| Path | Purpose |
|---|---|
| `app/Http/Controllers/Admin/ResearchArticleController.php` | Six CRUD methods. No private helpers — no taxonomy to load. |
| `resources/views/admin/research_articles/index.blade.php` | DataTable list. |
| `resources/views/admin/research_articles/create.blade.php` / `edit.blade.php` | Page wrappers. |
| `resources/views/admin/research_articles/partials/form.blade.php` | Shared form (title, author, publish date, article body). |
| `resources/views/admin/research_articles/partials/form_scripts.blade.php` | CKEditor 5 on `#research-article-input`, Flatpickr on publish date. **No Choices.js** — no select widgets to enhance. |

### Modified

| Path | Change |
|---|---|
| `app/Models/ResearchArticle.php` | Replaced `$guarded = []` with explicit `$fillable` (5 columns). Added `$casts` (`publish_date` → `datetime`). Default `$timestamps = true` retained. |
| `routes/web.php` | Imported `AdminResearchArticleController`; added `admin.research-articles.*` route group inside the `auth + role:super-admin` middleware, after samasyas. |
| `resources/views/admin/partials/sidebar.blade.php` | Added `$isResearchArticles`; included in `$contentOpen`; wired the dead `#` link. |

### Routes registered

```
GET|HEAD   admin/research-articles                          admin.research-articles.index
GET|HEAD   admin/research-articles/create                   admin.research-articles.create
POST       admin/research-articles/data_table               admin.research-articles.data_table
POST       admin/research-articles/remove                   admin.research-articles.remove
POST       admin/research-articles/save                     admin.research-articles.save
GET|HEAD   admin/research-articles/{researchArticle}/edit   admin.research-articles.edit
```

---

## 5. Form Layout

1. **Row 1:** Title (full width, required, max 255)
2. **Row 2:** Author (6 cols, required, max 255) · Publish date (3 cols, required, Flatpickr)
3. **Row 3:** Article body (full width, CKEditor 5, required)

> **2026-05-17 tweak:** Author and Publish date were promoted from optional to required (same change applied to the Articles form for consistency). Existing rows with NULL `author` or `publish_date` will fail validation if someone tries to re-save them without populating those fields — fine for going-forward edits, but worth knowing.
4. **Footer:** "Back to list" · "Save Research Article" / "Update Research Article"

DataTable columns:

`S.No. | Title | Author | Publish | Action`

Searchable across `title`, `author`, `article_strip_tag`. Default sort `id DESC`.

---

## 6. Verification

| Check | Result |
|---|---|
| `php -l` on `ResearchArticleController.php`, `ResearchArticle.php`, `routes/web.php` | No syntax errors. |
| `php artisan route:list --name=admin.research-articles` | 6 routes registered. |
| Blade compile + render harness | All views compile. (`$errors`-harness warning is 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
- TIMESTAMP `publish_date` round-trips correctly (date in, midnight back out, displays cleanly)
- `updated_at` is bumped on edit (was it before, with `$guarded = []`? Yes — but worth verifying after the `$fillable` switch)
- CKEditor + Flatpickr init cleanly

---

## 7. Follow-ups (not done)

- Journals admin CRUD — last remaining dead Content sidebar link with a known model. The schema is more involved (it has `journal_sections`) so it'll need a real design pass, not a copy.
- Same no-tests rationale as previous plans.
