# Circulars Admin CRUD — Plan & Build Record

> **Status (2026-05-17):** ✅ Implemented. This document was written alongside the implementation as a retrospective plan + reference.
>
> **Goal:** Give the Circulars content type the same admin CRUD surface that Articles already has — index + DataTable, separate create page, separate edit page, save, remove — using the `circulars` table's own schema, and wire the previously-dead "Circulars" sidebar link.
>
> **Pattern reference:** This is a direct mirror of the Articles admin module (`Admin\ArticleController` + `resources/views/admin/articles/`). See `backendwork.md` §4 Phase 3 for the broader phase context.

---

## 1. Schema

`circulars` table (from `jotici (1).sql`):

```sql
CREATE TABLE `circulars` (
  `id`                INT(11)   NOT NULL,
  `publish_date`      DATE      NOT NULL,
  `title`             TEXT          DEFAULT NULL,
  `date`              DATE      NOT NULL,
  `notification_no`   TEXT          DEFAULT NULL,
  `gazette`           TEXT          DEFAULT NULL,
  `subject_id`        INT(10)   NOT NULL,                 -- FK → circular_subject.id
  `details_strip_tag` LONGTEXT      DEFAULT NULL,
  `details`           LONGTEXT      DEFAULT NULL,         -- rich-text body
  `created_at`        TIMESTAMP NOT NULL DEFAULT current_timestamp()
                                 ON UPDATE current_timestamp()
);
```

Key differences vs. `articles`:

| Aspect | Articles | Circulars |
|---|---|---|
| Taxonomy | category + subcategory (2-level) | subject only (1-level, NOT NULL) |
| Title length | `VARCHAR(768)` | `TEXT` (capped at 65,535 in validation) |
| Date fields | one (`publish_date`) | **two** — `date` (issue date on the document) **and** `publish_date` (site publication date), both NOT NULL |
| Extra metadata | author | `notification_no`, `gazette` |
| Body column | `article` / `article_strip_tag` | `details` / `details_strip_tag` |

The two-date split was confirmed with the user before designing the form.

---

## 2. Architecture Decisions

| Decision | Choice | Reason |
|---|---|---|
| CRUD shape | Page-based (`create.blade.php`, `edit.blade.php`), not modal | Matches Articles — both have a rich-text body that doesn't fit comfortably in a modal. Acts / Sections / Subjects stay modal because they're simple text fields. |
| Controller methods | `index`, `getDataTable`, `create`, `edit`, `save`, `remove` | Same six-method surface as `Admin\ArticleController`. |
| Validation | Inline `Validator::make` inside `save()` | Same project convention — JSON endpoints use `back()->withErrors()`, FormRequests don't suit that flow. (See `backendwork.md` §1 status row.) |
| `details_strip_tag` | Recomputed on every save from `strip_tags($details)` + whitespace collapse | Mirrors how `article_strip_tag` is maintained. Used for DataTable full-text search. |
| Subject relationship | Reuse existing `CircularSubject` admin module as the taxonomy source | Already exists under CMS sidebar group; no new taxonomy work. |
| Sidebar | Replace dead `<a href="#">Circulars</a>` (line 67) with real route; add to `$contentOpen` flag | The Content group's collapse behaviour needs `$isCirculars` mixed into `$contentOpen` so the group stays open on circulars routes. |

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

- User-facing circulars listing/view — already exists at `UserController@circulars`, unchanged.
- Research Articles / Amendments / Samasyas / Journals admin CRUD — other dead sidebar links stay dead. Separate tickets.
- Changes to `CircularSubject` admin (the subject lookup table) — left as-is.
- Changes to circulars export (`UserController@exportCircularPdf` / `exportCircularWord`) — untouched.

---

## 3. Validation Rules (in `CircularController::save`)

```php
'id'              => 'nullable|integer',
'subject_id'      => 'required|integer|exists:circular_subject,id',
'title'           => 'required|string|max:65535',
'date'            => 'required|date',          // "Circular Date" (issue date)
'publish_date'    => 'required|date',          // "Publish Date" (site publication)
'notification_no' => 'nullable|string|max:65535',
'gazette'         => 'nullable|string|max:65535',
'details'         => 'required|string|max:5000000',
```

Attribute aliases provided so error messages read naturally (e.g. `subject_id` → "subject", `notification_no` → "notification number").

No cross-field constraint analogous to Articles' "subcategory must belong to category" — Circular has no equivalent.

---

## 4. Files

### Added

| Path | Purpose |
|---|---|
| `app/Http/Controllers/Admin/CircularController.php` | The six CRUD methods. |
| `resources/views/admin/circulars/index.blade.php` | DataTable list page. |
| `resources/views/admin/circulars/create.blade.php` | New-circular page wrapper. |
| `resources/views/admin/circulars/edit.blade.php` | Edit-circular page wrapper. |
| `resources/views/admin/circulars/partials/form.blade.php` | The shared form (subject, title, notification no., gazette, both dates, details). |
| `resources/views/admin/circulars/partials/form_scripts.blade.php` | CKEditor 5 on `#circular-details`, Flatpickr on both date inputs, Choices.js on subject select. |

### Modified

| Path | Change |
|---|---|
| `app/Models/Circular.php` | Added `$fillable` (matches schema) + `$casts` (`subject_id`→int, `date`+`publish_date`→date, `created_at`→datetime). `$timestamps = false` retained (no `updated_at` column). `subject()` relation already present. |
| `routes/web.php` | Imported `AdminCircularController`; added `admin.circulars.*` route group inside the existing `auth + role:super-admin` middleware, immediately after the articles group. |
| `resources/views/admin/partials/sidebar.blade.php` | Added `$isCirculars = request()->routeIs('admin.circulars.*')`; included it in `$contentOpen`; replaced the dead `#` link with `route('admin.circulars.index')` plus `active` class. |

### Routes registered

```
GET|HEAD   admin/circulars                  admin.circulars.index
GET|HEAD   admin/circulars/create           admin.circulars.create
POST       admin/circulars/data_table       admin.circulars.data_table
POST       admin/circulars/remove           admin.circulars.remove
POST       admin/circulars/save             admin.circulars.save
GET|HEAD   admin/circulars/{circular}/edit  admin.circulars.edit
```

---

## 5. Form Layout

1. **Row 1:** Subject (4 cols, required) · Title (8 cols, required)
2. **Row 2:** Notification No. (6 cols, optional) · Gazette (6 cols, optional)
3. **Row 3:** Circular Date (3 cols, required, Flatpickr `dd-mm-Y` ↔ `Y-m-d`) · Publish Date (3 cols, required, Flatpickr)
4. **Row 4:** Details (full width, CKEditor 5 Classic, min-height 500px)
5. **Footer:** "Back to list" link · "Save Circular" / "Update Circular" button

DataTable columns:

`S.No. | Subject | Title | Notification No. | Circular Date | Publish | Action`

Searchable across `title`, `notification_no`, `gazette`, `details_strip_tag`, `circular_subject.subject`.

Default sort: `circulars.id DESC` (server-side, set when the request specifies no sort column).

---

## 6. Verification

Run during build:

| Check | Result |
|---|---|
| `php -l` on `CircularController.php`, `Circular.php`, `routes/web.php` | No syntax errors. |
| `php artisan route:list --name=admin.circulars` | 6 routes registered. |
| Blade compile of all 5 new views | All compile. |

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

- End-to-end create / edit / delete through the admin UI
- DataTable AJAX payload shape on a populated table
- CKEditor + Flatpickr + Choices.js init on the form page (no JS console errors)
- Sidebar Content group auto-opens on circulars routes
- Validation error rendering on the create/edit page

---

## 7. Follow-ups (not done)

- Other dead Content sidebar links (Research Articles, Amendments, Samasyas, Journals) — same pattern, separate tickets.
- A `Subject` column header sort on the index DataTable uses `circular_subject.subject` directly; works in MySQL but if the project ever moves to strict ANSI mode this would need a `COALESCE`.
- No tests added — project has only `tests/{Feature,Unit}/ExampleTest.php` placeholders; introducing tests for one module would be inconsistent with the rest of the codebase. Worth a separate "add admin CRUD test harness" ticket.
