# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## What this is An LMS / CBT (Computer-Based Test) application for employee training compliance, built on **Laravel 13** with **Livewire 3 + Volt**. The codebase is a rewrite ("v2") of an older CodeIgniter 3 system; data is migrated in from the legacy database via Artisan commands (see Data Migration below). Most code comments and many domain terms are in Indonesian. ## Commands ```bash composer dev # Run the full dev stack concurrently: php artisan serve + queue:listen + pail (logs) + vite npm run dev # Vite dev server only (HMR for assets) npm run build # Production asset build composer test # Clears config cache, then runs the full PHPUnit suite php artisan test # Run tests directly php artisan test --filter=AuthenticationTest # Run a single test class php artisan test tests/Feature/Auth/PasswordResetTest.php # Run a single file vendor/bin/pint # Format PHP (Laravel Pint, default ruleset — no config file) ``` Tests run against an in-memory **SQLite** database (configured in `phpunit.xml`), independent of the MySQL dev database. ## Architecture ### Routing & access control (read `routes/web.php` first) Authorization is layered through nested route-group middleware, not per-controller checks: 1. `auth` — must be logged in. 2. `force.password` (`App\Http\Middleware\ForcePasswordChange`, aliased in `bootstrap/app.php`) — if `User->must_change_password` is true, every request is redirected to `password.force-change` until the user sets a new password. This implements mandatory first-login password rotation for migrated accounts. 3. Spatie role middleware splits the app into areas: - `role:admin|trainer` → `/admin` (prefix `admin.`) and `/reports` — management UI, master data, question bank, exports/imports. - `role:karyawan` → `/portal` (route names prefixed `cbt.`) — the employee test-taking portal. `bootstrap/app.php` is where middleware aliases and the global `web` middleware stack are registered (Laravel 11+ style — there is no `Http/Kernel.php`). ### Three user roles `admin`, `trainer`, `karyawan` (employee). Roles are managed by **spatie/laravel-permission** (`role:` middleware, `HasRoles` trait on `User`). Note: `User` also has a legacy `role` string column that older migration code writes to directly — prefer the Spatie role API for new authorization logic. ### Volt page routing `VoltServiceProvider` mounts both `resources/views/livewire` and `resources/views/pages` as Volt component roots. Auth screens (`routes/auth.php`, Laravel Breeze + Volt) and the password-reset flow are single-file Volt components under `resources/views/pages/auth`. Controllers in `app/Http/Controllers/{Admin,Cbt}` mostly return Blade views (`resources/views/pages/...`) that embed Livewire components for interactivity. ### Domain model - **Master data:** `Department` ⇄ `Position` (many-to-many via `department_position`); a `User` belongs to one department and one position. - **TrainingMatrix** ties a `Department` + `Position` + `Sop` together — it defines which SOP/training a role must complete (compliance requirement). - **Sop** — standard operating procedure / training material. Uses `spatie/laravel-activitylog` (`getActivitylogOptions`). - **Exam / QuestionBank / Question** — `QuestionBank` is scoped by department/position/training_matrix; exams produce `ExamResult` records (`is_passed`, `user_id`, `exam_id`). - **Compliance** is computed (not stored): `App\Livewire\Matrix\ComplianceMonitor` derives the pass rate as `karyawan` users with ≥1 passing `ExamResult` ÷ total `karyawan`. - `UserDocument` — uploaded employee documents. When relating to `User`, note the legacy migration columns `old_id` and `old_password_hash` used to map/verify accounts carried over from the CI3 system. ### Activity logging Login/logout are logged via `Event::listen` in `AppServiceProvider::boot()` (category `autentikasi`). Models use spatie/activitylog for audit trails, surfaced through `AuditTrailController`. ### File storage — Nextcloud over WebDAV `AppServiceProvider::boot()` registers a custom `webdav` Storage driver (Sabre DAV client + Flysystem). The `nextcloud` disk in `config/filesystems.php` uses it, configured via `NEXTCLOUD_*` env vars. Use `Storage::disk('nextcloud')` for employee document/photo storage. ### Imports / exports Employee, department, and position data export to Excel (**maatwebsite/excel**, `app/Exports`) and PDF (**barryvdh/laravel-dompdf**). Employee import has a preview→process two-step flow (`app/Imports`, see `EmployeeController` routes). Custom export/import routes are declared **before** `Route::resource(...)` in `web.php` so they aren't shadowed by resource wildcards — preserve that ordering when adding routes. ## Data Migration (legacy CI3 → v2) A second MySQL connection **`mysql_old`** (env `DB_*_OLD`, default database `lmsv2-old`) holds the legacy data. The migration is run by `php artisan lms:migrate-data` (`app/Console/Commands/MigrateLmsData.php`), which orchestrates, in strict order: departments (from `sections`) → positions → department-position links → staff/students → user dept/position sync → questions → exam history. Additional one-off commands exist: `lms:migrate-*` (`MigrateOldQuestions`, `MigrateStaffToTrainer`, `MigrateUserInitials`, `MigrateUserKtp`). These read the old DB directly with `DB::table('lmsv2-old.')`. > Note: the repo is **not** a git repository. There is a stray file named ``elect('SHOW COLUMNS FROM `lmsv2-old`.classes');`` in the project root — a scratch artifact, not source.