# FAPI — Faculty Appraisal & Performance Index

A configurable, multi-tier faculty appraisal platform for higher-education institutions. FAPI manages appraisal campaigns, reusable question banks, multi-tier review pipelines (self → HOD → Director → …), section-based evaluations, faculty enrollment/assignment, S3 file uploads, external module API integrations, and aggregated reporting.

The repository is split into two independently deployable applications:

| Directory          | Stack                                                                                       | Role                                                              |
| ------------------ | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| [`api/`](api/)     | PHP, Slim Framework v3, MySQL, Phinx, PHPUnit                                               | REST API (JWT-auth, served under the `/fapi/` Apache prefix)      |
| [`web/`](web/)     | Vue 3 (Composition API + `<script setup>`), Pinia v3, Vue Router 4, Vite, TypeScript strict | Single-page app served from `/faculty-appraisal/`                 |

---

## Architecture at a Glance

```
Browser ─ /faculty-appraisal/ ─▶ web/  (Vue 3 SPA, server-driven menu)
                                  │  Axios + JWT
                                  ▼
Apache    ─ /fapi/             ─▶ api/public/index.php
                                  │
                                  ▼
Slim v3 API  (com\linways\fapi)
  ├─ JWT middleware     ──▶ $GLOBALS (userId, role, departmentId, …)
  ├─ /api/v1/{module}/  ──▶ controller → service (singleton) → mapper → DTO
  └─ Permission gate via `permissions_{methodName}` on the controller
                                  │
                                  ▼
MySQL  (fapi_* tables)  +  shared `staffaccounts`, `college_menu_*` tables
                            S3 (via `lin_resource`) for uploads
                            External module APIs for dynamic question data
```

### Core entities

`Application` → `ReviewTier` × N, `Section` × N → `Question` (linked via `SectionQuestion`).
`FacultyAssignment` (admin → faculty) → `FacultyEnrollment` (faculty opts in) → `TierSubmission` × N → `QuestionResponse`.

Independent / cross-cutting: `Question` (reusable bank), `EvaluatorRule` (parameterized SQL that resolves evaluators dynamically), `ModuleApi` (external data sources injected into questions).

### Submission lifecycle

```
locked ──(previous tier submitted)──▶ not_started
                                          │
                                   (first auto-save by evaluator;
                                    atomic claim of submission)
                                          ▼
                                     in_progress
                                          │
                                   (hard submit — all required
                                    fields validated)
                                          ▼
                                      submitted ──(admin reopen)──┐
                                                                  │
                                                  ▲───────────────┘
```

`submitted_at` is set only on transition to `submitted` and is `NULL` for every other state.
Once a `fapi_tier_submission` is `submitted`, its child `fapi_question_response` rows are immutable unless an admin reopens it.

---

## Repository Layout

```
fapi/
├── api/                              ← PHP / Slim 3 REST API
│   ├── bootstrap/
│   │   ├── app.php                   ← Slim app bootstrap (defines SOURCE_DIR)
│   │   ├── controllers.php           ← DI container registrations
│   │   ├── middlewares.php           ← JWT → $GLOBALS, CORS
│   │   └── routes.php                ← Mounts /v1 group
│   ├── public/index.php              ← Apache entrypoint
│   ├── src/com/linways/
│   │   ├── core/
│   │   │   ├── dto/                  ← Application, ReviewTier, Section, Question, …
│   │   │   ├── mapper/               ← ResultMap definitions per module
│   │   │   ├── request/              ← Search*Request value objects
│   │   │   ├── exception/            ← FapiException (single class, many constants)
│   │   │   └── service/              ← Singleton services (BaseService → MySqlQuery)
│   │   └── v1/{module}/
│   │       ├── controller/           ← {Module}Controller extends BaseController
│   │       └── routes.php            ← Module route group
│   │   (includes v1/docs/ ← OpenAPI/Swagger UI, DEBUG-gated, no DB-backed module)
│   ├── db/migrations/                ← 20 Phinx migrations (fapi_* schema)
│   ├── menu_toggles/                 ← Admin / faculty / evaluator menu seed migrations
│   ├── test/                         ← PHPUnit tests
│   ├── composer.json
│   ├── phinx.yml
│   └── lcli                          ← CLI wrapper for migrations / seeds
│
├── web/                              ← Vue 3 SPA
│   ├── src/
│   │   ├── main.ts                   ← Pinia → layouts → router → auth → mount
│   │   ├── App.vue                   ← Dynamic layout via route.meta.layout
│   │   ├── helpers/                  ← Axios, AuthHelper, ApiEndPoints (~90 endpoints)
│   │   ├── router/                   ← index.ts + routeMap.ts + PublicUrls.ts
│   │   ├── stores/                   ← 13 Pinia stores (composition API)
│   │   ├── composables/              ← useAutoSave, useConfirm, useFormValidation, …
│   │   ├── constants/FapiConstants.ts
│   │   ├── layouts/                  ← DefaultLayout (sidebar) / PlainLayout
│   │   ├── pages/                    ← ErrorPage.vue
│   │   ├── components/common/        ← Header, Sidebar, BreadCrumb, Footer
│   │   ├── modules/                  ← admin / faculty / evaluator / report / shared / common
│   │   ├── types/                    ← 10 TypeScript interface files
│   │   └── utils/menuToRoutes.ts     ← menu API response → Vue Router routes
│   ├── vite.config.ts                ← base = '/faculty-appraisal/'
│   ├── package.json                  ← pnpm only (postinstall re-creates @linwaysams/common symlink)
│   └── scripts/                      ← fix-linways-symlink.cjs
│
├── .gitignore
└── README.md
```

---

## Quick Start

### Prerequisites

- PHP 7.x+ with the usual extensions (`pdo_mysql`, `mbstring`, `json`)
- Composer
- MySQL 5.7+ / MariaDB 10.x
- Node ≥ 20.19 (or ≥ 22.12)
- **pnpm 9.x** — npm / yarn are not supported (a postinstall hook fixes a `@linwaysams/common` symlink that those package managers break)
- Apache configured so that `/fapi/` → `api/public/index.php` and `/faculty-appraisal/` → `web/dist/`

### Backend (`api/`)

```bash
cd api
composer install
cp .cli.env .env                                 # set DB_PROFESIONAL_CONFIG, JWT secret, S3 keys
./lcli db:migrate -all                           # run all Phinx migrations + menu seeds
./vendor/bin/phpunit                             # optional — run tests
```

Apache maps `/fapi/` → `api/public/index.php`; Slim then mounts a `/api/v1` group on top, so the full external path is `https://<host>/fapi/api/v1/{module}/...`.

### Frontend (`web/`)

```bash
cd web
pnpm install                                     # postinstall restores @linwaysams/common symlink
pnpm dev                                         # Vite dev server
# or
pnpm build                                       # vue-tsc type-check + production build → dist/
```

Set `VITE_API_BASE_URL` in `web/.env` to point at the Slim API. The SPA is served from `/faculty-appraisal/` (set in `vite.config.ts`).

---

## Key Concepts

### Backend (`api/`)

- **Namespace root**: `com\linways\fapi\…` (replaces the older `com\linways\wm\…`). PSR-4 autoload maps `com\linways\fapi\api\v1\` → `src/com/linways/v1` and `com\linways\fapi\core\` → `src/com/linways/core`.
- **Table prefix**: `fapi_*`. Every table carries audit columns (`created_by`, `created_date`, `updated_by`, `updated_date`); FK columns follow `<referenced_table>_id`; booleans are `TINYINT(1)` with `is_` / `has_` prefix; marks use `DECIMAL(7,2)` (never `FLOAT`/`DOUBLE`); JSON columns use `JSON COMMENT 'JSON'` so the mapper can pick them up with `OBJECT_FROM_JSON`.
- **Auth**: JWT (HS512) via `linways/linways-auth`. After the middleware runs, `$GLOBALS['userId' | 'userType' | 'role' | 'departmentId' | 'batchId' | 'semesterId' | 'collegeCode']` are available in every service and controller.
- **Singleton services** that extend `BaseService` (which extends `MySqlQuery`). Always call `ServiceClass::getInstance()` — never `new`. Thirteen services in total: `ApplicationService`, `BaseService`, `EnrollmentService`, `EvaluatorRuleService`, `EvaluatorService`, `FacultyAssignmentService`, `ModuleApiService`, `QuestionService`, `ReportService`, `ReviewTierService`, `SectionService`, `TierSubmissionService`, `UploadService`. The three highest-connectivity hubs (per the graphify knowledge graph) are `TierSubmissionService` (29 edges — the evaluation engine), `ApplicationService`, and `EnrollmentService` — touch them carefully, most other modules depend on them.
- **Per-tier question overrides**: `SectionService::saveQuestionTierConfigs()` / `getQuestionTierConfigs()` / `getTierRequirementMap()` manage `fapi_section_question_tier_config`, letting a question's `remarks_required` / `file_upload_required` flags be overridden per review tier (e.g. HOD must attach a file, self-appraisal doesn't). Edited via `SectionTierMatrix.vue` on the frontend.
- **API docs**: `DocsController` (`/docs/` and `/docs/ui/`) serves a live OpenAPI 3 spec (via `zircote/swagger-php` annotation scanning) and a Swagger UI page — both gated behind `DEBUG=true` and 404 otherwise.
- **Prepared statements only.** Use `executeQueryUsingPreparedStatement` / `…ForListUsingPreparedStatement` / `…ForObjectUsingPreparedStatement` with `?` placeholders. Never interpolate user input into SQL. Service methods still call `realEscapeObject()` / `realEscapeString()` at the top as a defence-in-depth layer.
- **Permissions** declared on controllers as `public $permissions_{methodName} = ['PERMISSION_CODE', …]` (OR-logic — any one grants access). Override with `public $isPermissionsRequired = false;` for unauthenticated endpoints.
- **Responses**: `Linways\Slim\Utils\ResponseUtils::result($response, $data)` on success, `ResponseUtils::fault($response, $e)` on error.
- **Validation**: `respect/validation` chained via `v::attribute(...)->assert($dto)`.

Detailed backend playbook (DTO/mapper/service templates, exception code catalogue, migration patterns, lifecycle rules, full route + permission matrix) lives in [`api/CLAUDE.md`](api/CLAUDE.md).

### Frontend (`web/`)

- **Vue 3 + `<script setup lang="ts">`** exclusively — no Options API, no class components.
- **Pinia v3** for state with the composition API setup-function pattern. Thirteen stores: `authStore`, `menuStore`, `application.store`, `question.store`, `section.store`, `reviewTier.store`, `evaluatorRule.store`, `enrollment.store`, `submission.store`, `facultyAssignment.store`, `moduleApi.store`, `report.store`, `ThemeConfigurations`.
- **Server-driven routing**: routes are not hardcoded. After login, `menuStore.getMenuList()` fetches `college_menu_items` rows; `utils/menuToRoutes.ts` transforms them; `router.addRoute()` registers each one with a lazy-loaded component looked up in `src/router/routeMap.ts` keyed by the menu item's `code`. A handful of static detail/form routes (application detail, evaluation list, evaluation form, error pages) are declared directly in `router/index.ts`.
- **URL prefixes**: SPA at `/faculty-appraisal/`, API at `/fapi/`. Never put `/fapi/` in a Vue route path — Apache will steal it.
- **HTTP**: shared `helpers/Axios.ts` instance with auth interceptors; every endpoint string lives in `helpers/ApiEndPoints.ts`. Stores normalize responses via `unwrapApiResponse()` / `getErrorMessage()` from `composables/responseFormatter.ts`.
- **Validation**: Vuelidate (`@vuelidate/core` + `@vuelidate/validators`).
- **UX primitives**: `$.showNotification`, `$.simpleDialog` jQuery plugins from the Pages framework (the same UI conventions as `crm-ui`); SCSS + Bootstrap (no Tailwind).
- **Shared lib**: `@linwaysams/common` (git dependency). Always import shared components (`AdvancedCard`, `SliderModal`, etc.) from there — do not fork local copies. A `postinstall` script repairs the symlink that pnpm flattens.
- **Rich text** via `@tinymce/tinymce-vue` (self-hosted, not the cloud CDN build — see `tinymce` dependency and `TinyMCE self hosted` commit); **file uploads** via `vue-filepond` direct-to-S3 then registered in `lin_resource` server-side.
- **Excel export**: `SummaryReport.vue` exports the rendered report table via `@linways/table-to-excel` (v2 — git dependency, no longer the older Vite-workaround build).
- **Per-tier question requirements**: `SectionTierMatrix.vue` (admin) edits the `remarks_required` / `file_upload_required` overrides per section-question-tier combination; `QuestionTierCell.vue` / `ReportSummaryTable.vue` (report) and `FacultyDetailReport.vue` (per-faculty detail page, route `/report/faculty/:enrollmentId`) render them.

Detailed frontend playbook (component map, store actions, types, menu-item registration checklist, v-model conventions, container-vs-presentational rules, base-URL pitfalls) lives in [`web/CLAUDE.md`](web/CLAUDE.md).

---

## Permission Model

Five permission codes gate everything (controller-level via `permissions_{methodName}`; menu visibility via `college_menu_groups.permission_code` / `college_menu_items.permission_code`):

| Code                    | Surfaces                                                                                          |
| ----------------------- | ------------------------------------------------------------------------------------------------- |
| `FAPI_QUESTION_BANK`    | Question CRUD                                                                                     |
| `FAPI_APPLICATIONS`     | Application admin: tiers, sections, enrollments, assignments, evaluator rules, submission reopen  |
| `FAPI_MODULE_APIS`      | External module-API definitions                                                                   |
| `FAPI_FACULTY`          | Faculty self-enrollment, evaluation form, module report data                                      |
| `FAPI_EVALUATOR_REVIEW` | Evaluator dashboard, evaluation form access, submission                                           |

A user needs **at least one** of the listed codes for a given route.

---

## API Surface (summary)

The Slim app mounts a `/api/v1` group, then ten module groups underneath it. Full route map with per-method permissions in [`api/CLAUDE.md`](api/CLAUDE.md).

| Group                  | What it covers                                                                                                                       |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `/docs/`               | OpenAPI JSON spec + Swagger UI (no permission required, but 404s unless `DEBUG=true`)                                                |
| `/common/`             | Departments, staff search (no permission required)                                                                                   |
| `/question/`           | Question-bank CRUD                                                                                                                   |
| `/application/`        | Application CRUD + lifecycle toggles (`publish`, `status`, `results-publish`) + nested tier & section CRUD                           |
| `/enrollment/`         | Available apps, self-enroll, enrollment listing + progress                                                                            |
| `/submission/`         | Evaluation form, autosave, hard submit, admin reopen                                                                                  |
| `/evaluator/`          | Evaluator overview aggregations + per-app submissions                                                                                 |
| `/evaluator-rule/`     | Evaluator rule listing                                                                                                                |
| `/faculty-assignment/` | Single + bulk faculty assignment, removal                                                                                             |
| `/module-api/`         | External API definitions + report-data fetch (with JWT forwarding)                                                                    |
| `/report/`             | Summary report (`/summary`) + per-faculty detailed report (`/faculty/{enrollmentId}`) — both `FAPI_APPLICATIONS`                       |

---

## Database

Twenty Phinx migrations build the full `fapi_*` schema:

```
fapi_question, fapi_application, fapi_review_tier, fapi_section,
fapi_section_question, fapi_section_tier, fapi_faculty_enrollment,
fapi_tier_submission, fapi_evaluator_rule, fapi_tier_eligible_evaluator,
fapi_question_response, fapi_module_api, fapi_faculty_assignment,
fapi_section_question_tier_config
```

Beyond the original 13 tables, later migrations add: `richtext_content` on `fapi_question_response`; `fapi_section_question_tier_config` (per-tier `remarks_required` / `file_upload_required` overrides, unique on `(section, question, tier)`, FK-constrained to all three parents); a data migration that seeds `fapi_module_api` rows from active `exat_form` (`entity_type = 'STAFF'`) records so EXAT reporting forms surface as module-API-linked questions; and a seed of a default `HOD` evaluator rule (`fapi_evaluator_rule.code = 'HOD'`) resolving the requesting staff's HOD via `v4_ams_staff_department_relations` + `roles`.

Plus three menu-toggle migrations under `api/menu_toggles/` that register the admin, faculty, and evaluator menu groups + items in the shared `college_menu_groups` / `college_menu_items` tables — these are what the SPA reads at boot to assemble the user's sidebar and dynamic routes.

Run them all with:

```bash
cd api && ./lcli db:migrate -all
```

> **Caveat — two Phinx configs in the tree.** Both `api/phinx.yml` and `api/db/phinx.yml` exist, with overlapping `migrations` / `seeds` paths, the same `db_migrations` change-log table, and the same `development` default environment. They appear to be duplicates of each other; only one is canonical. When changing migration setup, update both (or consolidate them) — otherwise the two configs will drift and Phinx behaviour will depend on which file is picked up by the CLI invocation.

---

## Implementation Status

**Phase 1 — complete on both sides:**

1. Question Bank — CRUD with all field types (marks / textarea / file / richtext / module-API-linked).
2. Application Management — full lifecycle (`draft → active → closed → archived`), publish + results-publish toggles, date-range validation.
3. Sections — display ordering, question linking, tier associations (application-scoped only).
4. Review Tiers — JSON evaluator-resolution rules, tier visibility config, unique ordering per application.
5. Faculty Assignment — admin single + bulk assignment with staff search.
6. Faculty Self-Enrollment — atomic: creates enrollment + all tier submissions + eligible-evaluator rows in one transaction.
7. Tier Submission Workflow — `locked → not_started → in_progress → submitted`, atomic evaluator claim, debounced autosave, hard submit with validation, admin reopen.
8. Evaluator Rule Engine — parameterized SQL with `[[placeholder]]` substitution and non-SELECT guards.
9. Evaluator Dashboard — overview aggregation + per-app submission list.
10. Module API Integration — external API definitions, report-data fetching with JWT forwarding.
11. File Upload — S3 via `ResourceService`, `lin_resource` registration, presigned URLs, FilePond on the frontend.
12. Reporting — `ReportService` (15 methods) backs both `/report/summary` (per-application, per-tier aggregated scores via `aggregateSummaryRows` / `pivotSummaryRows` / `computeTierGrandTotals`) and `/report/faculty/{enrollmentId}` (detailed per-faculty breakdown via `assembleDetailedSections`, `resolveFilesForGrouped`, `loadStaffName`); frontend `SummaryReport.vue` (with `ReportSummaryTable.vue` / `QuestionTierCell.vue`) and `FacultyDetailReport.vue` consume the two endpoints.
13. Per-Tier Question Requirements — `fapi_section_question_tier_config` lets `remarks_required` / `file_upload_required` be overridden per section-question-tier combination (e.g. a file is mandatory only at the HOD tier); edited via `SectionTierMatrix.vue`, enforced by `TierSubmissionService` validation on hard submit.
14. Excel Export — `SummaryReport.vue` exports the report table to `.xlsx` via `@linways/table-to-excel` v2.
15. API Docs — OpenAPI spec + Swagger UI at `/docs/` and `/docs/ui/`, gated behind `DEBUG=true`.
16. EXAT Module Integration — data migration auto-registers active EXAT report forms as `fapi_module_api` entries; a seeded default `HOD` evaluator rule resolves the HOD evaluator dynamically from department relations.

> **Note:** the `api/CLAUDE.md` file still describes the reporting dashboard as Phase 2 / unimplemented. That description is stale — the controller, service, request DTO (`SearchReportRequest`), and routes all exist on disk. CLAUDE.md needs a refresh on this point.

**Phase 2 — deferred** (see `api/phase-2-plan.md`):

1. Application eligibility criteria via the reserved `fapi_application.properties` JSON column (department / role / teaching-type filtering).

> Excel/PDF export, originally listed here, has since shipped (see item 14 above) — only the eligibility-criteria item remains outstanding.

---

## Conventions Cheat-Sheet

### Backend — do not

- interpolate user input into SQL strings — always `?` placeholders bound by the prepared-statement helpers.
- skip `realEscapeObject()` / `realEscapeString()` at the top of public service methods.
- initialize a mapper inside a method — always in the constructor as `$this->mapper`.
- write to `fapi_question_response` once the parent `fapi_tier_submission.status` is `submitted` — the guards in `autoSave()` and `submit()` raise `ALREADY_SUBMITTED`. The only way to revise marks is for an admin to call `reopen()` first, which flips status back to `in_progress`; `reopen()` itself does not edit responses, it just unlocks the normal write paths.
- delete a `fapi_question` that has linked `fapi_section_question` or `fapi_question_response` rows.
- use `FLOAT` / `DOUBLE` for marks columns (use `DECIMAL(7,2)`).
- share `Section` records across applications — sections are application-scoped; only `fapi_question` definitions are reused.

### Frontend — do not

- use `npm` or `yarn` (pnpm only).
- use `/fapi/` as a route prefix (that's the API; the SPA lives at `/faculty-appraisal/`).
- access a Pinia store from a presentational/leaf component (pass data via props, communicate via kebab-case emits).
- hardcode a menu-driven route (register an entry in `routeMap.ts` and a row in `college_menu_items`).
- use the Options API (always `<script setup lang="ts">`).
- fork copies of `@linwaysams/common` components (`AdvancedCard`, `SliderModal`, etc.) — always import from the shared lib.
- commit `package-lock.json` or `yarn.lock`.
- use Tailwind — SCSS + Bootstrap classes that match the Pages framework.

---

## Further Reading

- [`api/CLAUDE.md`](api/CLAUDE.md) — backend conventions, step-by-step module template, full route + permission + exception map, submission lifecycle rules.
- [`web/CLAUDE.md`](web/CLAUDE.md) — frontend conventions, store catalogue, component map, server-driven routing flow, menu seed SQL.
- `api/phase-2-plan.md` — scoped plan for deferred Phase 2 work.
- `api/graphify-out/` and `web/graphify-out/` — knowledge graphs of each side of the codebase (queryable with `graphify query "..."`).
