# Department Self-Service Portals — Implementation Report

## 1. Backend changes

- **Migration** `2026_05_20_100000_add_department_portal_fields.php`: adds `slug`, `portal_enabled`, `portal_description_en/ar`, `portal_theme`, `portal_requires_membership` on `departments` (unique `company_id` + `slug`).
- **Backfill** `2026_05_20_100001_backfill_department_slugs.php`: generates slugs for existing departments from `code`/`name`.
- **Model** `Department`: new fillable fields and casts.
- **Validation** `DepartmentSlugRule`: lowercase URL-safe slugs; rejects reserved words (`admin`, `api`, `login`, `auth`, `profile`, `tickets`, `settings`, `search`, `notifications`).
- **Support** `DepartmentSlugGenerator`: auto-generate and ensure unique slugs per company.
- **Service** `DepartmentPortalService`: resolve portal by slug, metadata, membership checks, portal ticket create (department from slug only), scoped my-tickets and profile my-tickets.
- **Controllers** `DepartmentPortalController`, `ProfileMyTicketsController`.
- **Request** `CreateDepartmentPortalTicketRequest`: prohibits `department_id` in payload.
- **Admin** `DepartmentAdminService` + department form requests/resources updated for portal fields.

## 2. Frontend changes

- **Feature module** `resources/js/src/features/departmentPortal/`:
  - Pages: `DepartmentPortalPage`, `DepartmentPortalCreateTicketPage`, `DepartmentPortalMyTicketsPage`, `ProfileMyTicketsPage`
  - Components: hero, ticket form/list/card, filters, layout
  - Services/hooks: `departmentPortalService`, `useDepartmentPortal`, `useCreateDepartmentPortalTicket`, `useDepartmentPortalMyTickets`, `useProfileMyTickets`
- **Routes** registered in `Router.tsx` (public landing; protected create/my-tickets and profile list).
- **Login redirect**: `ProtectedRoute` and `LoginPage` honor `?redirect=` and return users to portal URLs after OTP login.
- **Admin** `AdminDepartmentsPage`: portal enabled, slug (super admin), descriptions EN/AR, membership requirement, copy portal link.
- **Profile** quick link to `/profile/my-tickets`.
- **i18n** `departmentPortal` namespace (EN/AR).

## 3. Routes added

| Layer | Route |
|-------|--------|
| Frontend | `/d/:departmentSlug` |
| Frontend | `/d/:departmentSlug/create-ticket` |
| Frontend | `/d/:departmentSlug/my-tickets` |
| Frontend | `/profile/my-tickets` |
| API | `GET /api/v1/department-portals/{slug}` |
| API | `POST /api/v1/department-portals/{slug}/tickets` (auth) |
| API | `GET /api/v1/department-portals/{slug}/my-tickets` (auth) |
| API | `GET /api/v1/profile/my-tickets` (auth) |

## 4. Data model changes

| Column | Type | Notes |
|--------|------|--------|
| `slug` | string, nullable → backfilled | Unique per `company_id` |
| `portal_enabled` | boolean, default true | Disabled portals return 404 |
| `portal_description_en` | text, nullable | Landing copy |
| `portal_description_ar` | text, nullable | Landing copy |
| `portal_theme` | json, nullable | Optional branding |
| `portal_requires_membership` | boolean, default false | Restricts authenticated access |

Department `name` is exposed as both `name_en` and `name_ar` in portal metadata (single `name` column today).

## 5. Security rules

- **Department ID never accepted from portal create payload** — resolved only from slug; `department_id` is `prohibited`.
- **Portal my-tickets**: `requester_id = auth user` AND `department_id = portal department`.
- **Profile my-tickets**: `requester_id = auth user` across all departments; optional `department_id` filter.
- **Portal disabled** or inactive: 404.
- **Restricted portal**: 403 if `portal_requires_membership` and user is not a department member (super admin bypasses).
- **Cross-company**: users cannot access another company’s portal.
- **Admin ticket management** unchanged — department heads use admin/dashboard routes, not requester portal.

## 6. Tests added

| Test file | Coverage |
|-----------|----------|
| `tests/Feature/Identity/DepartmentPortalApiTest.php` | Metadata, create, department scoping, isolation, disabled/restricted portals |
| `tests/Feature/Identity/ProfileMyTicketsTest.php` | Cross-department requester tickets, auth required |
| `tests/Feature/Identity/DepartmentSlugValidationTest.php` | Reserved slug, uniqueness, auto-generate |
| `resources/js/src/__tests__/unit/features/departmentPortal/DepartmentPortalPage.test.tsx` | Loading/error UI |
| `resources/js/src/__tests__/e2e/department-portal-smoke.spec.ts` | Playwright landing smoke |

## 7. Commands executed

```bash
php artisan migrate --force
php artisan test --filter='DepartmentPortalApiTest|ProfileMyTicketsTest|DepartmentSlugValidationTest'
npm run type-check
npm run build
npm run test -- --run resources/js/src/__tests__/unit/features/departmentPortal
```

**Results:** Backend portal tests 12/12 passed. `npm run type-check` and `npm run build` succeeded. Portal unit tests 2/2 passed.

**Note:** Full `npm run lint` reports a pre-existing unused import in `AdminUsersPage.test.tsx` (not introduced by this work).

## 8. Known gaps

- Attachments on portal create ticket are accepted in validation but not yet wired to the existing attachment upload flow.
- `portal_theme` is stored/admin-editable as JSON only; no dedicated theme UI beyond optional JSON in admin (not exposed in modal yet).
- Bilingual department **names** still use a single `name` field (descriptions are bilingual).
- E2E smoke only checks landing visibility; full login → create → list flow needs auth fixtures for `/d/it`.
- Magic link login post-auth redirect not updated (OTP path supports `?redirect=`).

## 9. UAT readiness

**Ready for UAT** with the caveats above: core flows (portal landing, login redirect, create ticket by slug, department-scoped my tickets, profile all tickets, admin portal settings, slug validation) are implemented and covered by automated API tests. Recommend UAT checklist:

1. Enable portal on a department and set slug (e.g. `it`).
2. Visit `/d/it` (guest and logged-in).
3. Create ticket; confirm department assignment.
4. Verify `/d/it/my-tickets` shows only own tickets for that department.
5. Verify `/profile/my-tickets` shows tickets across departments.
6. Test restricted portal with `portal_requires_membership`.
7. Test disabled portal returns unavailable state.
