Initial commit - lms-v2 + CLAUDE.md

This commit is contained in:
Iwit
2026-05-30 22:15:16 +07:00
commit 5811409e2d
183 changed files with 23225 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[{compose,docker-compose}.{yml,yaml}]
indent_size = 4
+65
View File
@@ -0,0 +1,65 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
# PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
+11
View File
@@ -0,0 +1,11 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore
+12
View File
@@ -0,0 +1,12 @@
/vendor
/node_modules
/.env
/.env.backup
/storage/framework
/storage/logs
/storage/*.key
/public/storage
/public/hot
*.log
.DS_Store
Thumbs.db
+2
View File
@@ -0,0 +1,2 @@
ignore-scripts=true
audit=true
+67
View File
@@ -0,0 +1,67 @@
# 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.<table>')`.
> 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.
+58
View File
@@ -0,0 +1,58 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Learning Laravel
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
In addition, [Laracasts](https://laracasts.com) contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
You can also watch bite-sized lessons with real-world projects on [Laravel Learn](https://laravel.com/learn), where you will be guided through building a Laravel application from scratch while learning PHP fundamentals.
## Agentic Development
Laravel's predictable structure and conventions make it ideal for AI coding agents like Claude Code, Cursor, and GitHub Copilot. Install [Laravel Boost](https://laravel.com/docs/ai) to supercharge your AI workflow:
```bash
composer require laravel/boost --dev
php artisan boost:install
```
Boost provides your agent 15+ tools and skills that help agents build Laravel applications while following best practices.
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
+311
View File
@@ -0,0 +1,311 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use App\Models\User;
class MigrateLmsData extends Command
{
protected $signature = 'lms:migrate-data';
protected $description = 'Migrasi komprehensif: Master Data -> Users -> Sinkronisasi Jabatan -> Bank Soal -> History Test';
public function handle()
{
$this->info('Memulai pemindahan data dari CI3 Lama ke Laravel 13 V2...');
// 1. Eksekusi Master Data Terlebih Dahulu
$this->migrateDepartments();
$this->migratePositions();
$this->migrateDepartmentPositions();
// 2. Eksekusi Data User
$this->migrateStaffToUsers();
$this->migrateStudentsToUsers();
// 3. Sinkronisasi Departemen & Posisi ke User
$this->syncUserDepartmentAndPosition();
// 4. Eksekusi Ujian & Hasil
$this->migrateQuestions();
$this->migrateExamHistory();
$this->info('Sukses! Semua data Master, Karyawan, Sinkronisasi Jabatan, Bank Soal, dan Histori Test berhasil dipindahkan.');
}
/**
* TAHAP 1A: Memindahkan data `sections` (Lama) ke `departments` (Baru)
*/
/**
* TAHAP 1A: Memindahkan data `sections` (Lama) ke `departments` (Baru)
*/
private function migrateDepartments()
{
$this->warn('1A. Menarik data Departemen (Sections)...');
$oldSections = DB::table('lmsv2-old.sections')->get();
$bar = $this->output->createProgressBar(count($oldSections));
$bar->start();
foreach ($oldSections as $section) {
// Generate kode otomatis untuk Departments karena kolom 'code' WAJIB ada
$code = 'DPT-' . str_pad($section->id, 3, '0', STR_PAD_LEFT);
DB::table('lmsv2.departments')->updateOrInsert(
['id' => $section->id],
[
'code' => $code, // Tabel ini punya kolom 'code'
'name' => trim($section->section),
'created_at' => $section->created_at ?? now(),
'updated_at' => now(),
]
);
$bar->advance();
}
$bar->finish();
$this->newLine();
}
/**
* TAHAP 1B: Memindahkan data `classes` (Lama) ke `positions` (Baru)
*/
private function migratePositions()
{
$this->warn('1B. Menarik data Posisi/Jabatan (Classes)...');
$oldClasses = DB::table('lmsv2-old.classes')->get();
$bar = $this->output->createProgressBar(count($oldClasses));
$bar->start();
foreach ($oldClasses as $class) {
DB::table('lmsv2.positions')->updateOrInsert(
['id' => $class->id],
[
// Tabel ini HANYA punya id, name, created_at, updated_at
// (Pastikan Anda sudah menjalankan Langkah 1 untuk menambah kolom 'name')
'name' => trim($class->class),
'created_at' => $class->created_at ?? now(),
'updated_at' => now(),
]
);
$bar->advance();
}
$bar->finish();
$this->newLine();
}
/**
* TAHAP 2A: Memindahkan data `staff` (Trainer/Admin) ke tabel `users`
*/
private function migrateStaffToUsers()
{
$this->warn('2A. Menarik data Staff (Trainer/Admin)...');
$oldStaffs = DB::connection('mysql_old')->table('staff')->get();
$bar = $this->output->createProgressBar(count($oldStaffs));
$bar->start();
foreach ($oldStaffs as $staff) {
$email = !empty($staff->email) ? $staff->email : $staff->employee_id . '@tunggal-pharma.com';
$gender = null;
if (strtolower($staff->gender) == 'male') $gender = 'L';
if (strtolower($staff->gender) == 'female') $gender = 'P';
User::updateOrCreate(
['email' => $email],
[
'nik' => $staff->employee_id,
'first_name' => trim($staff->name),
'last_name' => trim($staff->surname),
'gender' => $gender,
'phone' => $staff->contact_no,
'password' => Hash::make('Tunggal123!'),
'role' => 'trainer',
'old_id' => $staff->id,
'old_password_hash' => $staff->password,
]
);
$bar->advance();
}
$bar->finish();
$this->newLine();
}
/**
* TAHAP 2B: Memindahkan data `students` (Karyawan) ke tabel `users`
*/
private function migrateStudentsToUsers()
{
$this->warn('2B. Menarik data Karyawan (Students)...');
$oldStudents = DB::connection('mysql_old')->table('students')->get();
$bar = $this->output->createProgressBar(count($oldStudents));
$bar->start();
foreach ($oldStudents as $student) {
$email = !empty($student->email) ? $student->email : $student->admission_no . '@tunggal-pharma.com';
$gender = null;
if (strtolower($student->gender) == 'male') $gender = 'L';
if (strtolower($student->gender) == 'female') $gender = 'P';
User::updateOrCreate(
['email' => $email],
[
'nik' => $student->admission_no,
'first_name' => trim($student->firstname),
'last_name' => trim($student->lastname),
'gender' => $gender,
'phone' => $student->mobileno,
'date_of_birth' => $student->dob,
'password' => Hash::make('Karyawan123!'),
'role' => 'karyawan',
'old_id' => $student->id,
]
);
$bar->advance();
}
$bar->finish();
$this->newLine();
}
/**
* TAHAP 3: MENGHUBUNGKAN USER DENGAN DEPARTEMEN & POSISI
*/
private function syncUserDepartmentAndPosition()
{
$this->warn('3. Menyinkronkan Karyawan dengan Departemen & Posisi...');
// Membaca relasi dari tabel student_session di DB lama
$oldSessions = DB::connection('mysql_old')->table('student_session')->get();
$bar = $this->output->createProgressBar(count($oldSessions));
$bar->start();
foreach ($oldSessions as $session) {
// Cari user di database baru berdasarkan old_id
$user = User::where('old_id', $session->student_id)
->where('role', 'karyawan')
->first();
if ($user) {
// Update jabatan dan departemen
// Asumsi: ID Departemen dan Posisi sama dengan ID Sections dan Classes dari DB Lama
$user->update([
'position_id' => $session->class_id,
'department_id' => $session->section_id,
]);
}
$bar->advance();
}
$bar->finish();
$this->newLine();
}
/**
* TAHAP 4A: Memindahkan data Bank Soal
*/
private function migrateQuestions()
{
$this->warn('4A. Menarik data Bank Soal...');
$oldQuestions = DB::connection('mysql_old')->table('questions')->get();
$bar = $this->output->createProgressBar(count($oldQuestions));
$bar->start();
foreach ($oldQuestions as $q) {
DB::table('question_banks')->updateOrInsert(
['old_id' => $q->id],
[
'question_text' => $q->question,
'option_a' => $q->opt_a,
'option_b' => $q->opt_b,
'option_c' => $q->opt_c,
'option_d' => $q->opt_d,
'option_e' => $q->opt_e,
'correct_answer' => $q->correct,
]
);
$bar->advance();
}
$bar->finish();
$this->newLine();
}
/**
* TAHAP 4B: Memindahkan data Ujian & Hasil Historis Karyawan
*/
private function migrateExamHistory()
{
$this->warn('4B. Menarik data Historis Nilai Ujian...');
// Tarik Master Ujian
$oldExams = DB::connection('mysql_old')->table('onlineexam')->get();
foreach ($oldExams as $exam) {
DB::table('exams')->updateOrInsert(
['old_id' => $exam->id],
[
'title' => $exam->exam,
'duration' => $exam->duration,
'passing_percentage' => $exam->passing_percentage,
]
);
}
// Tarik Hasil Test Karyawan
$oldResults = DB::connection('mysql_old')
->table('onlineexam_students')
->join('student_session', 'onlineexam_students.student_session_id', '=', 'student_session.id')
->join('students', 'student_session.student_id', '=', 'students.id')
->select('onlineexam_students.*', 'students.id as old_student_id')
->get();
$bar = $this->output->createProgressBar(count($oldResults));
$bar->start();
foreach ($oldResults as $result) {
$user = DB::table('users')->where('old_id', $result->old_student_id)->where('role', 'karyawan')->first();
$exam = DB::table('exams')->where('old_id', $result->onlineexam_id)->first();
if ($user && $exam) {
DB::table('exam_results')->updateOrInsert(
[
'user_id' => $user->id,
'exam_id' => $exam->id
],
[
'is_attempted' => $result->is_attempted,
'created_at' => $result->created_at,
]
);
}
$bar->advance();
}
$bar->finish();
$this->newLine();
}
/**
* TAHAP 1C: Memindahkan pemetaan class_sections (Lama) ke department_position (Baru)
*/
private function migrateDepartmentPositions()
{
$this->warn('1C. Menarik data Relasi Departemen & Posisi (Class Sections)...');
$oldRelations = \Illuminate\Support\Facades\DB::table('lmsv2-old.class_sections')->get();
$bar = $this->output->createProgressBar(count($oldRelations));
$bar->start();
foreach ($oldRelations as $relation) {
// Asumsi kolom lama bernama section_id dan class_id
\Illuminate\Support\Facades\DB::table('lmsv2.department_position')->updateOrInsert(
[
'department_id' => $relation->section_id,
'position_id' => $relation->class_id,
]
);
$bar->advance();
}
$bar->finish();
$this->newLine();
}
}
@@ -0,0 +1,94 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use App\Models\QuestionBank;
use App\Models\User;
class MigrateOldQuestions extends Command
{
protected $signature = 'lms:migrate-questions';
protected $description = 'Menyalin data soal dari lmsv2-old.questions ke lmsv2.question_banks secara presisi';
public function handle()
{
$this->info('Memulai Sinkronisasi Bank Soal dari Database Lama...');
$oldQuestions = DB::table('lmsv2-old.questions')->get();
if ($oldQuestions->isEmpty()) {
$this->warn('Tidak ada data di tabel lmsv2-old.questions!');
return;
}
$bar = $this->output->createProgressBar(count($oldQuestions));
$bar->start();
$successCount = 0;
foreach ($oldQuestions as $old) {
// 1. Pemetaan Tipe Soal
$type = 'single';
$oldType = strtolower($old->question_type ?? '');
if (str_contains($oldType, 'multiple')) { $type = 'multiple'; }
elseif (str_contains($oldType, 'true') || str_contains($oldType, 'boolean')) { $type = 'true_false'; }
elseif (str_contains($oldType, 'desc') || str_contains($oldType, 'essay')) { $type = 'descriptive'; }
// 2. Pemetaan Level
$level = 'sedang';
$oldLevel = strtolower($old->level ?? '');
if (str_contains($oldLevel, 'mudah') || str_contains($oldLevel, 'easy')) { $level = 'mudah'; }
elseif (str_contains($oldLevel, 'sulit') || str_contains($oldLevel, 'hard')) { $level = 'sulit'; }
// 3. Pemetaan Pembuat (Creator)
$creatorId = null;
if (isset($old->staff_id)) {
$oldStaff = DB::table('lmsv2-old.staff')->where('id', $old->staff_id)->first();
if ($oldStaff && !empty($oldStaff->email)) {
$user = User::where('email', $oldStaff->email)->first();
if ($user) $creatorId = $user->id;
}
}
// 4. Penanganan Kunci Jawaban (Karena di Model di-cast sebagai Array)
$correctAnswer = null;
if (!empty($old->correct)) {
// Dibungkus ke dalam array agar tidak error saat Laravel mencoba json_encode
$correctAnswer = [$old->correct];
}
// 5. Simpan menggunakan format nama kolom yang BARU
QuestionBank::updateOrCreate(
['old_id' => $old->id], // Pencocokan menggunakan old_id
[
'question_type' => $type,
'question_level' => $level,
'question_text' => $old->question,
// Opsi A sampai E
'option_a' => $old->opt_a,
'option_b' => $old->opt_b,
'option_c' => $old->opt_c,
'option_d' => $old->opt_d,
'option_e' => $old->opt_e,
'correct_answer' => $correctAnswer, // Sudah dalam bentuk array
'created_by' => $creatorId,
'created_at' => $old->created_at ?? now(),
'updated_at' => $old->updated_at ?? now(),
]
);
$successCount++;
$bar->advance();
}
$bar->finish();
$this->newLine();
$this->info("Sinkronisasi Selesai! Berhasil memindahkan {$successCount} Soal beserta kunci jawabannya.");
}
}
@@ -0,0 +1,59 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use App\Models\User;
use Spatie\Permission\Models\Role; // Pastikan library Role dari Spatie dipanggil
class MigrateStaffToTrainer extends Command
{
protected $signature = 'lms:migrate-trainers';
protected $description = 'Mencocokkan data staff lama dan menetapkannya sebagai Trainer & Trainee';
public function handle()
{
$this->info('Memastikan Role (Hak Akses) tersedia di database...');
// Membuat role otomatis jika belum ada di tabel 'roles'
Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']);
Role::firstOrCreate(['name' => 'trainer', 'guard_name' => 'web']);
Role::firstOrCreate(['name' => 'trainee', 'guard_name' => 'web']);
$this->info('Memulai pencocokan data Staff (Trainer)...');
$oldStaffs = DB::table('lmsv2-old.staff')->get();
$bar = $this->output->createProgressBar(count($oldStaffs));
$bar->start();
$successCount = 0;
foreach ($oldStaffs as $staff) {
$matchedUser = null;
// Prioritas 1: Pencocokan Email
if (!empty($staff->email)) {
$matchedUser = User::where('email', $staff->email)->first();
}
// Prioritas 2: Pencocokan Nama
if (!$matchedUser && !empty($staff->name)) {
$matchedUser = User::where(DB::raw("CONCAT(first_name, ' ', COALESCE(last_name, ''))"), 'LIKE', "%{$staff->name}%")->first();
}
// Jika ketemu, tetapkan role-nya
if ($matchedUser) {
// assignRole() kini dijamin aman karena role sudah dibuat di awal script
$matchedUser->assignRole(['trainer', 'trainee']);
$successCount++;
}
$bar->advance();
}
$bar->finish();
$this->newLine();
$this->info("Selesai! {$successCount} karyawan berhasil diperbarui sebagai Trainer.");
}
}
@@ -0,0 +1,76 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class MigrateUserInitials extends Command
{
protected $signature = 'lms:migrate-initials';
protected $description = 'Migrasi kolom roll_no dari tabel student lama dengan pencocokan Email / Nama karyawan';
public function handle()
{
$this->info('Memulai tarik data inisial (roll_no) dengan pencocokan pintar...');
// Ambil data student dari database lama yang memiliki roll_no
$oldStudents = DB::table('lmsv2-old.students')->whereNotNull('roll_no')->get();
$bar = $this->output->createProgressBar(count($oldStudents));
$bar->start();
$successCount = 0;
$failCount = 0;
foreach ($oldStudents as $oldStudent) {
// Ubah object ke array agar lebih aman saat mengecek keberadaan kolom (mencegah error jika kolom tidak ada)
$old = (array) $oldStudent;
$matchedUser = null;
// PRIORITAS 1: Cocokkan berdasarkan Email (Paling Akurat)
if (!empty($old['email'])) {
$matchedUser = DB::table('lmsv2.users')->where('email', $old['email'])->first();
}
// PRIORITAS 2: Jika email tidak cocok/kosong, cocokkan berdasarkan Nama
if (!$matchedUser) {
$query = DB::table('lmsv2.users');
// Cek apakah database lama menggunakan 'first_name' atau 'name' tunggal
if (!empty($old['first_name'])) {
$query->where('first_name', $old['first_name']);
if (!empty($old['last_name'])) {
$query->where('last_name', $old['last_name']);
}
} elseif (!empty($old['name'])) {
// Jika DB lama hanya punya 1 kolom 'name'
$query->where(DB::raw("CONCAT(first_name, ' ', COALESCE(last_name, ''))"), 'LIKE', "%{$old['name']}%");
} else {
// Tidak ada indikator nama yang bisa dicari
$query->where('id', '<', 0); // Force empty result
}
$matchedUser = $query->first();
}
// JIKA KETEMU: Update tabel users yang baru
if ($matchedUser) {
DB::table('lmsv2.users')
->where('id', $matchedUser->id) // Update berdasarkan ID baru yang ditemukan
->update(['initial' => $old['roll_no']]);
$successCount++;
} else {
$failCount++;
}
$bar->advance();
}
$bar->finish();
$this->newLine();
$this->info("Selesai! Berhasil memetakan: {$successCount} data. Tidak ditemukan: {$failCount} data.");
}
}
+79
View File
@@ -0,0 +1,79 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class MigrateUserKtp extends Command
{
protected $signature = 'lms:migrate-ktp';
protected $description = 'Migrasi data nomor KTP dari adhar_no atau note berdasarkan Email/Nama';
public function handle()
{
$this->info('Memulai pemetaan Nomor KTP (Identity Number)...');
// Ambil dari database lama yang adhar_no ATAU note-nya ada isinya
$oldStudents = DB::table('lmsv2-old.students')
->where(function($query) {
$query->whereNotNull('adhar_no')->where('adhar_no', '!=', '')
->orWhereNotNull('note')->where('note', '!=', '');
})->get();
$bar = $this->output->createProgressBar(count($oldStudents));
$bar->start();
$successCount = 0;
$failCount = 0;
foreach ($oldStudents as $oldStudent) {
$old = (array) $oldStudent;
// Prioritaskan adhar_no, jika kosong gunakan note
$ktpNumber = !empty($old['adhar_no']) ? $old['adhar_no'] : $old['note'];
$matchedUser = null;
// PRIORITAS 1: Pencocokan Email
if (!empty($old['email'])) {
$matchedUser = DB::table('lmsv2.users')->where('email', $old['email'])->first();
}
// PRIORITAS 2: Pencocokan Nama
if (!$matchedUser) {
$query = DB::table('lmsv2.users');
if (!empty($old['first_name'])) {
$query->where('first_name', $old['first_name']);
if (!empty($old['last_name'])) {
$query->where('last_name', $old['last_name']);
}
} elseif (!empty($old['name'])) {
$query->where(DB::raw("CONCAT(first_name, ' ', COALESCE(last_name, ''))"), 'LIKE', "%{$old['name']}%");
} else {
$query->where('id', '<', 0); // Skip jika data nama rusak
}
$matchedUser = $query->first();
}
// JIKA KETEMU: Update kolom identity_number
if ($matchedUser) {
DB::table('lmsv2.users')
->where('id', $matchedUser->id)
->update(['identity_number' => $ktpNumber]);
$successCount++;
} else {
$failCount++;
}
$bar->advance();
}
$bar->finish();
$this->newLine();
$this->info("Pembaruan KTP Selesai! Berhasil terpetakan: {$successCount} data. Tidak ditemukan (Nama/Email beda): {$failCount} data.");
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace App\Exports;
use Maatwebsite\Excel\Concerns\FromArray;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
class EmployeeTemplateExport implements FromArray, WithHeadings, ShouldAutoSize
{
public function headings(): array
{
// PENTING: Header ini dibaca oleh Controller, jangan diubah formatnya
return [
'nik', 'no_ktp', 'inisial', 'nama_depan', 'nama_belakang',
'email', 'telepon', 'jenis_kelamin', 'tgl_lahir', 'tgl_masuk',
'department_id', 'position_id'
];
}
public function array(): array
{
return [
// Baris contoh (Gunakan format YYYY-MM-DD untuk tanggal)
['10102023', '3201012345678901', 'BDS', 'Budi', 'Santoso', 'budi@perusahaan.com', '0812345678', 'L', '1990-01-30', '2023-01-01', '1', '2'],
['10102024', '', 'STA', 'Siti', 'Aminah', 'siti@perusahaan.com', '0887654321', 'P', '1995-12-15', '2023-05-10', '1', '3'],
];
}
}
+89
View File
@@ -0,0 +1,89 @@
<?php
namespace App\Exports;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithCustomStartCell;
use Maatwebsite\Excel\Events\AfterSheet;
use Carbon\Carbon;
class EmployeesExport implements FromCollection, WithHeadings, WithMapping, ShouldAutoSize, WithEvents, WithCustomStartCell
{
protected $employees;
// Menangkap data hasil filter dari Controller
public function __construct($employees)
{
$this->employees = $employees;
}
public function collection()
{
return $this->employees;
}
// Mulai tabel data di baris ke-6 (Baris 1-4 untuk informasi header)
public function startCell(): string
{
return 'A6';
}
public function headings(): array
{
return ['NIK', 'No. KTP', 'Inisial', 'Nama Depan', 'Nama Belakang', 'Email', 'No. HP', 'Jenis Kelamin', 'Tgl Lahir', 'Tgl Masuk', 'Departemen', 'Jabatan', 'Status', 'Hak Akses'];
}
public function map($user): array
{
return [
$user->nik,
$user->identity_number ?? '-',
strtoupper($user->initial ?? '-'),
$user->first_name,
$user->last_name,
$user->email,
$user->phone ?? '-',
$user->gender == 'L' ? 'Laki-laki' : 'Perempuan',
$user->date_of_birth ? Carbon::parse($user->date_of_birth)->format('Y-m-d') : '-',
$user->join_date ? Carbon::parse($user->join_date)->format('Y-m-d') : '-',
$user->department->name ?? '-',
$user->position->name ?? '-',
$user->is_active ? 'Aktif' : 'Non-Aktif',
$user->roles->pluck('name')->implode(', '),
];
}
// Event untuk menyuntikkan Teks Header di atas tabel dan memberi warna
public function registerEvents(): array
{
return [
AfterSheet::class => function(AfterSheet $event) {
$sheet = $event->sheet->getDelegate();
$count = $this->employees->count();
$user = auth()->user()->first_name . ' ' . auth()->user()->last_name;
$date = Carbon::now()->translatedFormat('d F Y - H:i:s');
// Menyuntikkan Informasi di baris atas
$sheet->setCellValue('A1', 'LAPORAN DATA KARYAWAN');
$sheet->setCellValue('A2', 'Total Data (Sesuai Filter) : ' . $count . ' Karyawan');
$sheet->setCellValue('A3', 'Dicetak Oleh : ' . $user);
$sheet->setCellValue('A4', 'Waktu Cetak : ' . $date);
// Styling (Merge judul & warna header tabel)
$sheet->mergeCells('A1:N1');
$sheet->getStyle('A1')->getFont()->setBold(true)->setSize(14);
$sheet->getStyle('A2:A4')->getFont()->setBold(true);
$sheet->getStyle('A6:N6')->getFont()->setBold(true);
// Memberi warna latar abu-abu pada header tabel (Baris 6)
$sheet->getStyle('A6:N6')->getFill()
->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
->getStartColor()->setARGB('FFE2E8F0');
},
];
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
namespace App\Exports;
use App\Models\Position;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithCustomStartCell;
use Maatwebsite\Excel\Events\AfterSheet;
use Carbon\Carbon;
class PositionsExport implements FromCollection, WithHeadings, WithMapping, ShouldAutoSize, WithEvents, WithCustomStartCell
{
public function collection()
{
return Position::orderBy('name', 'asc')->get();
}
public function startCell(): string
{
return 'A6';
}
public function headings(): array
{
return ['No', 'Nama Jabatan / Posisi', 'Dibuat Pada'];
}
public function map($position): array
{
static $number = 0;
$number++;
return [
$number,
$position->name,
$position->created_at ? Carbon::parse($position->created_at)->format('d-m-Y H:i') : '-',
];
}
public function registerEvents(): array
{
return [
AfterSheet::class => function(AfterSheet $event) {
$sheet = $event->sheet->getDelegate();
$count = Position::count();
$user = auth()->user()->first_name . ' ' . auth()->user()->last_name;
$date = Carbon::now()->translatedFormat('d F Y - H:i:s');
$sheet->setCellValue('A1', 'LAPORAN MASTER DATA JABATAN');
$sheet->setCellValue('A2', 'Total Data : ' . $count . ' Jabatan');
$sheet->setCellValue('A3', 'Dicetak Oleh: ' . $user);
$sheet->setCellValue('A4', 'Waktu Cetak : ' . $date);
$sheet->mergeCells('A1:C1');
$sheet->getStyle('A1')->getFont()->setBold(true)->setSize(14);
$sheet->getStyle('A2:A4')->getFont()->setBold(true);
$sheet->getStyle('A6:C6')->getFont()->setBold(true);
$sheet->getStyle('A6:C6')->getFill()
->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
->getStartColor()->setARGB('FFE2E8F0');
},
];
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Spatie\Activitylog\Models\Activity;
use Illuminate\Http\Request;
class AuditTrailController extends Controller
{
public function index()
{
// Menarik data log terbaru, beserta relasi user yang melakukannya
$logs = Activity::with('causer')
->orderBy('created_at', 'desc')
->paginate(50);
return view('pages.admin.audit.index', compact('logs'));
}
}
@@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User;
use App\Models\Sop;
use App\Models\ExamResult;
use Illuminate\View\View;
class DashboardController extends Controller
{
/**
* 1. Fungsi Dashboard Utama Admin
*/
public function index(Request $request): View
{
$statistics = [
// PENTING: Menggunakan whereHas() untuk mengecek relasi Role di tabel pivot
'total_karyawan' => User::whereHas('roles', function ($query) {
$query->where('name', 'trainee'); // Atau 'karyawan', sesuaikan dengan nama role di database Anda
})->count(),
'total_trainer' => User::whereHas('roles', function ($query) {
// Menghitung admin dan trainer
$query->whereIn('name', ['trainer', 'admin']);
})->count(),
'total_sop' => Sop::where('status', 'Active')->count(),
'lulus_ujian' => ExamResult::where('is_passed', true)->count(),
];
// Pastikan path view ini benar-benar ada di resources/views/pages/admin/dashboard/dashboard.blade.php
return view('pages.admin.dashboard.dashboard', compact('statistics'));
}
/**
* 2. Fungsi untuk menu Departemen & Posisi (Master Data)
*/
public function masterIndex(): View
{
return view('pages.admin.master-data.index');
}
/**
* 4. Fungsi untuk menu Dokumen SOP
*/
public function sopIndex(): View
{
return view('pages.admin.sops.index');
}
}
@@ -0,0 +1,108 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Department;
use Illuminate\Http\Request;
use Barryvdh\DomPDF\Facade\Pdf; // Jika pakai DomPDF
use Maatwebsite\Excel\Facades\Excel; // Jika pakai Laravel-Excel
class DepartmentController extends Controller
{
public function index(Request $request)
{
// 1. Inisialisasi query builder dari model Department
$query = Department::query();
// 2. Fitur Pencarian (Filter)
if ($request->filled('search')) {
$search = $request->search;
$query->where('name', 'like', "%{$search}%")
->orWhere('code', 'like', "%{$search}%");
}
// 3. Eksekusi query dengan pagination dan pertahankan parameter URL
$departments = $query->latest()->paginate(10)->withQueryString();
// 4. Total rekor untuk ditampilkan di UI
$totalRecords = $departments->total();
return view('pages.admin.departments.index', compact('departments', 'totalRecords'));
}
public function create()
{
return view('pages.admin.departments.create');
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255|unique:departments,name',
'code' => 'nullable|string|max:50|unique:departments,code',
]);
Department::create($validated);
return redirect()->route('admin.departments.index')
->with('success', 'Departemen baru berhasil ditambahkan.');
}
public function show(Department $department)
{
// Hanya melempar object $department ke view tanpa load relasi positions
return view('pages.admin.departments.show', compact('department'));
}
public function edit(Department $department)
{
return view('pages.admin.departments.edit', compact('department'));
}
public function update(Request $request, Department $department)
{
$validated = $request->validate([
'name' => 'required|string|max:255|unique:departments,name,' . $department->id,
'code' => 'nullable|string|max:50|unique:departments,code,' . $department->id,
]);
$department->update($validated);
return redirect()->route('admin.departments.index')
->with('success', 'Data Departemen berhasil diperbarui.');
}
public function exportPdf(Request $request)
{
// 1. Ambil data sesuai filter/search saat ini
$search = $request->search;
$departments = Department::when($search, function($q) use ($search) {
$q->where('name', 'like', "%{$search}%");
})->get();
// 2. Tangkap Metadata Siapa & Kapan
$metadata = [
'downloaded_by' => auth()->user()->first_name . ' ' . auth()->user()->last_name, // Mengambil nama admin yang login
'download_time' => now()->translatedFormat('l, d F Y - H:i:s'), // Format waktu Indonesia
'total_data' => $departments->count()
];
// 3. Render View ke PDF
$pdf = Pdf::loadView('pages.admin.departments.export-pdf', compact('departments', 'metadata'));
return $pdf->download('Data_Departemen_'.date('YmdHis').'.pdf');
}
public function destroy(Department $department)
{
// Opsional: Cek apakah ada posisi terkait sebelum menghapus
if ($department->positions()->count() > 0) {
return back()->with('error', 'Gagal menghapus: Departemen ini masih memiliki Posisi/Jabatan aktif.');
}
$department->delete();
return redirect()->route('admin.departments.index')
->with('success', 'Departemen berhasil dihapus.');
}
}
@@ -0,0 +1,426 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Arr;
use App\Models\User;
use App\Models\Department;
use App\Models\Position;
use App\Models\TrainingMatrix;
class EmployeeController extends Controller
{
public function index(Request $request)
{
$chartDept = User::select('department_id', DB::raw('count(*) as total'))
->whereNotNull('department_id')
->with('department')
->groupBy('department_id')
->get();
$chartPos = User::select('position_id', DB::raw('count(*) as total'))
->whereNotNull('position_id')
->with('position')
->groupBy('position_id')
->get();
$departments = Department::with('positions')->get();
$positions = Position::all();
$deptPosMapping = $departments->mapWithKeys(function ($dept) {
return [$dept->id => $dept->positions->map(function($pos) {
return ['id' => $pos->id, 'name' => $pos->name];
})];
});
$query = User::with(['department', 'position', 'roles']);
if ($request->filled('search')) {
$search = $request->search;
$columns = \Illuminate\Support\Facades\Schema::getColumnListing('users');
$query->where(function($q) use ($search, $columns) {
foreach ($columns as $column) {
$q->orWhere($column, 'like', "%{$search}%");
}
});
}
if ($request->filled('department_id')) {
$query->where('department_id', $request->department_id);
}
if ($request->filled('position_id')) {
$query->where('position_id', $request->position_id);
}
$employees = $query->latest()->paginate(10)->withQueryString();
$totalRecords = $employees->total();
return view('pages.admin.employee.index', compact(
'employees', 'totalRecords', 'departments', 'positions', 'chartDept', 'chartPos', 'deptPosMapping'
));
}
public function create()
{
$departments = Department::with('positions')->orderBy('name', 'asc')->get();
$positions = Position::all();
$deptPosMapping = $departments->mapWithKeys(function ($dept) {
return [$dept->id => $dept->positions->map(function($pos) {
return ['id' => $pos->id, 'name' => $pos->name];
})];
});
$marketingDeptIds = Department::where('name', 'LIKE', '%MKT%')
->orWhere('name', 'LIKE', '%MARKETING%')
->pluck('id');
$trainingMatrices = TrainingMatrix::whereIn('department_id', $marketingDeptIds)->get();
$existingInitials = User::whereNotNull('initial')
->pluck('initial')
->map(fn($i) => strtolower($i))
->toArray();
return view('pages.admin.employee.create', compact(
'departments', 'positions', 'trainingMatrices', 'deptPosMapping', 'existingInitials'
));
}
public function store(Request $request)
{
$validated = $request->validate([
'nik' => 'required|unique:users,nik',
'identity_number' => 'nullable|string|max:50',
'initial' => 'nullable|string|max:10',
'first_name' => 'required|string|max:255',
'last_name' => 'nullable|string|max:255',
'phone' => 'nullable|string|max:20',
'email' => 'required|email|unique:users,email',
'role' => 'nullable|string',
'is_trainer' => 'nullable|boolean',
'gender' => 'required|in:L,P',
'date_of_birth' => 'nullable|date',
'join_date' => 'nullable|date',
'department_id' => 'required|exists:departments,id',
'position_id' => 'required|exists:positions,id',
'training_matrix_id' => 'nullable|integer',
'documents.*' => 'nullable|file|max:5120',
]);
DB::beginTransaction();
try {
$validated['password'] = Hash::make('Karyawan123!');
$validated['role'] = $request->role ?? 'karyawan';
$validated['must_change_password'] = true;
$userData = Arr::except($validated, ['documents', 'is_trainer']);
$user = User::create($userData);
$rolesToAssign = [];
if ($validated['role'] == 'admin') {
$rolesToAssign[] = 'admin';
} elseif ($validated['role'] == 'trainer') {
$rolesToAssign[] = 'trainer';
$rolesToAssign[] = 'trainee';
} else {
$rolesToAssign[] = 'trainee';
}
if ($request->has('is_trainer') && $request->is_trainer) {
$rolesToAssign[] = 'trainer';
$rolesToAssign[] = 'trainee';
}
$user->assignRole(array_unique($rolesToAssign));
if ($request->hasFile('documents')) {
foreach ($request->file('documents') as $file) {
$fileName = time() . '_' . str_replace(' ', '_', $file->getClientOriginalName());
$path = $file->storeAs('employee_docs/' . $user->nik, $fileName, 'nextcloud');
DB::table('employee_documents')->insert([
'user_id' => $user->id,
'file_name' => $file->getClientOriginalName(),
'file_path' => $path,
'created_at' => now(),
'updated_at' => now(),
]);
}
}
DB::table('audit_trails')->insert([
'user_id' => auth()->id(),
'action' => 'Create Employee',
'description' => "Menambahkan karyawan baru: {$user->first_name} {$user->last_name} (NIK: {$user->nik})",
'ip_address' => $request->ip(),
'created_at' => now(),
]);
DB::commit();
// FIX: Menggunakan rute jamak (employees)
return redirect()->route('admin.employees.index')
->with('success', "Data Karyawan {$user->first_name} berhasil ditambahkan! Password default: Karyawan123!");
} catch (\Exception $e) {
DB::rollBack();
return back()->with('error', 'Gagal menyimpan data! ' . $e->getMessage())->withInput();
}
}
public function show(User $employee)
{
$employee->load(['department', 'position', 'roles']);
return view('pages.admin.employee.show', compact('employee'));
}
public function edit(User $employee)
{
$departments = Department::with('positions')->orderBy('name', 'asc')->get();
$positions = Position::all();
$deptPosMapping = $departments->mapWithKeys(function ($dept) {
return [$dept->id => $dept->positions->map(function($pos) {
return ['id' => $pos->id, 'name' => $pos->name];
})];
});
$marketingDeptIds = Department::where('name', 'LIKE', '%MKT%')->orWhere('name', 'LIKE', '%MARKETING%')->pluck('id');
$trainingMatrices = TrainingMatrix::whereIn('department_id', $marketingDeptIds)->get();
return view('pages.admin.employee.edit', compact('employee', 'departments', 'positions', 'trainingMatrices', 'deptPosMapping'));
}
public function update(Request $request, User $employee)
{
$validated = $request->validate([
'nik' => ['required', Rule::unique('users', 'nik')->ignore($employee->id)],
'identity_number' => 'nullable|string|max:50',
'initial' => 'nullable|string|max:10',
'first_name' => 'required|string|max:255',
'last_name' => 'nullable|string|max:255',
'phone' => 'nullable|string|max:20',
'email' => ['required', 'email', Rule::unique('users', 'email')->ignore($employee->id)],
'role' => 'nullable|string',
'is_trainer' => 'nullable|boolean',
'gender' => 'required|in:L,P',
'date_of_birth' => 'nullable|date',
'join_date' => 'nullable|date',
'department_id' => 'required|exists:departments,id',
'position_id' => 'required|exists:positions,id',
'training_matrix_id' => 'nullable|integer',
'is_active' => 'nullable|boolean',
]);
DB::beginTransaction();
try {
if ($request->filled('password')) {
$validated['password'] = Hash::make($request->password);
}
$userData = Arr::except($validated, ['is_trainer', 'is_active']);
$userData['is_active'] = $request->has('is_active');
$employee->update($userData);
$rolesToAssign = [];
$inputRole = $validated['role'] ?? 'karyawan';
if ($inputRole == 'admin') {
$rolesToAssign[] = 'admin';
} elseif ($inputRole == 'trainer' || $request->is_trainer) {
$rolesToAssign[] = 'trainer';
$rolesToAssign[] = 'trainee';
} else {
$rolesToAssign[] = 'trainee';
}
$employee->syncRoles(array_unique($rolesToAssign));
DB::table('audit_trails')->insert([
'user_id' => auth()->id(),
'action' => 'Update Employee',
'description' => "Memperbarui data karyawan: {$employee->first_name} (NIK: {$employee->nik})",
'ip_address' => $request->ip(),
'created_at' => now(),
]);
DB::commit();
// FIX: Menggunakan rute jamak (employees)
return redirect()->route('admin.employees.index')->with('success', "Data Karyawan {$employee->first_name} berhasil diperbarui.");
} catch (\Exception $e) {
DB::rollBack();
return back()->with('error', 'Gagal memperbarui data! ' . $e->getMessage())->withInput();
}
}
public function destroy(User $employee)
{
if ($employee->hasRole('admin')) {
return back()->with('error', 'Aksi ditolak! Anda tidak dapat menonaktifkan akun Administrator.');
}
$nama = $employee->first_name;
$employee->update(['is_active' => false]);
DB::table('audit_trails')->insert([
'user_id' => auth()->id(),
'action' => 'Deactivate Employee',
'description' => "Menonaktifkan karyawan (Resign/Keluar): {$nama}",
'ip_address' => request()->ip(),
'created_at' => now(),
]);
// FIX: Menggunakan rute jamak (employees)
return redirect()->route('admin.employees.index')->with('success', "Karyawan {$nama} berhasil dinonaktifkan. Data historis tetap tersimpan.");
}
// =========================================================================
// FITUR EXPORT & IMPORT
// =========================================================================
private function getFilteredEmployees(Request $request)
{
$query = User::with(['department', 'position', 'roles'])->whereNotNull('department_id');
if ($request->filled('search')) {
$search = $request->search;
$columns = \Illuminate\Support\Facades\Schema::getColumnListing('users');
$query->where(function($q) use ($search, $columns) {
foreach ($columns as $column) {
$q->orWhere($column, 'like', "%{$search}%");
}
});
}
if ($request->filled('department_id')) {
$query->where('department_id', $request->department_id);
}
if ($request->filled('position_id')) {
$query->where('position_id', $request->position_id);
}
return $query->latest()->get(); // Ambil semua data TANPA pagination
}
public function exportExcel(Request $request)
{
// Ambil data yang sudah difilter
$employees = $this->getFilteredEmployees($request);
$fileName = 'Data_Karyawan_' . date('Y-m-d_H-i') . '.xlsx';
// Lempar data ke Class Export
return \Maatwebsite\Excel\Facades\Excel::download(new \App\Exports\EmployeesExport($employees), $fileName);
}
public function exportPdf(Request $request)
{
// Ambil data yang sudah difilter
$employees = $this->getFilteredEmployees($request);
// PENTING: Perhatikan penamaan folder (employee tanpa 's')
$pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('pages.admin.employee.pdf', compact('employees'))
->setPaper('a4', 'landscape');
return $pdf->download('Data_Karyawan_' . date('Y-m-d_H-i') . '.pdf');
}
public function importView()
{
return view('pages.admin.employee.import');
}
public function downloadTemplate()
{
// Menggunakan library Excel untuk menghasilkan format murni .xlsx
return \Maatwebsite\Excel\Facades\Excel::download(new \App\Exports\EmployeeTemplateExport, 'Template_Import_Karyawan.xlsx');
}
public function importPreview(Request $request)
{
$request->validate([
'file' => 'required|mimes:xlsx,xls,csv|max:5120'
]);
try {
$rows = \Maatwebsite\Excel\Facades\Excel::toArray(new class implements \Maatwebsite\Excel\Concerns\ToModel, \Maatwebsite\Excel\Concerns\WithHeadingRow {
public function model(array $row) { return $row; }
}, $request->file('file'))[0];
session()->put('import_rows_data', $rows);
return view('pages.admin.employee.import-preview', compact('rows'));
} catch (\Exception $e) {
return back()->with('error', 'Gagal membaca format file Excel. Pastikan menggunakan template yang benar. Error: ' . $e->getMessage());
}
}
public function importProcess(Request $request)
{
$rows = session('import_rows_data');
if (!$rows) {
return redirect()->route('admin.employees.import')->with('error', 'Sesi import telah kedaluwarsa atau kosong. Silakan unggah ulang file Anda.');
}
$successCount = 0;
$failCount = 0;
foreach ($rows as $row) {
try {
if (empty($row['nik']) || empty($row['nama_depan']) || empty($row['email'])) {
$failCount++;
continue;
}
$user = User::updateOrCreate(
['nik' => $row['nik']], // Jika NIK sama, perbarui. Jika tidak ada, buat baru.
[
'identity_number' => $row['no_ktp'] ?? null,
'initial' => !empty($row['inisial']) ? strtoupper($row['inisial']) : null,
'first_name' => $row['nama_depan'],
'last_name' => $row['nama_belakang'] ?? null,
'email' => $row['email'],
'phone' => $row['telepon'] ?? null,
'gender' => in_array(strtoupper($row['jenis_kelamin'] ?? ''), ['L', 'P']) ? strtoupper($row['jenis_kelamin']) : 'L',
'date_of_birth' => !empty($row['tgl_lahir']) ? \Carbon\Carbon::parse($row['tgl_lahir'])->format('Y-m-d') : null,
'join_date' => !empty($row['tgl_masuk']) ? \Carbon\Carbon::parse($row['tgl_masuk'])->format('Y-m-d') : null,
'department_id' => $row['department_id'] ?? 1,
'position_id' => $row['position_id'] ?? 1,
'password' => Hash::make('12345678'),
'is_active' => true,
'must_change_password' => true,
]
);
if (!$user->hasAnyRole(['admin', 'trainer', 'trainee'])) {
$user->assignRole('trainee');
}
$successCount++;
} catch (\Exception $e) {
$failCount++;
}
}
session()->forget('import_rows_data');
return redirect()->route('admin.employees.index')
->with('success', "Proses Import Selesai! Berhasil: {$successCount} data. Gagal/Dilewati: {$failCount} data.");
}
}
@@ -0,0 +1,91 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Department;
use App\Models\Position;
use App\Models\TrainingMatrix;
use App\Models\User;
use App\Models\QuestionBank;
class ExamManagementController extends Controller
{
/**
* Menampilkan halaman Bank Soal dengan Filter Komprehensif
*/
public function questionBank(Request $request)
{
// 1. Siapkan Data untuk Dropdown Filter
$departments = Department::orderBy('name', 'asc')->get();
$positions = Position::orderBy('name', 'asc')->get();
// $matrices = TrainingMatrix::orderBy('title', 'asc')->get();
// Ambil daftar user yang pernah membuat soal (untuk filter 'Pembuat')
// Sesuaikan nama relasi/kolom jika berbeda
$creators = User::whereHas('roles', function($q){
$q->whereIn('name', ['admin', 'trainer']);
})->orderBy('first_name', 'asc')->get();
// 2. Query Utama dengan Relasi
$query = QuestionBank::with(['department', 'position', 'creator']);
// 3. Logika Filter Dinamis
if ($request->filled('search')) {
$query->where('question_text', 'like', '%' . $request->search . '%');
}
if ($request->filled('department_id')) {
$query->where('department_id', $request->department_id);
}
if ($request->filled('position_id')) {
$query->where('position_id', $request->position_id);
}
// if ($request->filled('matrix_id')) {
// $query->where('training_matrix_id', $request->matrix_id);
// }
if ($request->filled('question_type')) {
$query->where('type', $request->question_type);
}
if ($request->filled('question_level')) {
$query->where('level', $request->question_level);
}
if ($request->filled('created_by')) {
$query->where('created_by', $request->created_by);
}
// 4. Eksekusi Query dengan Pagination
$questions = $query->latest()->paginate(15)->withQueryString();
$totalQuestions = $questions->total();
return view('pages.admin.exams.question-bank', compact(
'questions', 'totalQuestions', 'departments', 'positions', 'creators'
));
}
/**
* Menghapus banyak soal sekaligus (Bulk Delete)
*/
public function bulkDelete(Request $request)
{
$request->validate([
'question_ids' => 'required|array',
'question_ids.*' => 'exists:questions,id'
]);
try {
Question::whereIn('id', $request->question_ids)->delete();
return back()->with('success', count($request->question_ids) . ' Soal berhasil dihapus secara massal.');
} catch (\Exception $e) {
return back()->with('error', 'Gagal menghapus soal: ' . $e->getMessage());
}
}
/**
* Menampilkan Halaman Import Soal
*/
public function importView()
{
return view('pages.admin.exams.import-questions');
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Department;
use App\Models\Position;
use Illuminate\Http\Request;
class MasterDataController extends Controller
{
public function index()
{
$departments = Department::withCount('users')->get();
$positions = Position::withCount('users')->get();
return view('pages.admin.master.index', compact('departments', 'positions'));
}
public function storeDepartment(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'code' => 'required|string|unique:departments,code'
]);
Department::create($validated);
return back()->with('success', 'Departemen baru berhasil ditambahkan.');
}
public function storePosition(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255|unique:positions,name'
]);
Position::create($validated);
return back()->with('success', 'Jabatan baru berhasil ditambahkan.');
}
}
@@ -0,0 +1,95 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Position;
use App\Models\Department;
use Illuminate\Http\Request;
class PositionController extends Controller
{
public function index(Request $request)
{
$query = \App\Models\Position::query();
// Logika pencarian (jika ada input search)
if ($request->has('search')) {
$search = $request->search;
$query->where('name', 'like', "%{$search}%");
}
// Ambil data tanpa memuat relasi 'department'
$positions = $query->latest()->paginate(10);
return view('pages.admin.positions.index', compact('positions'));
}
public function create()
{
$departments = Department::orderBy('name', 'asc')->get();
return view('pages.admin.positions.create', compact('departments'));
}
public function store(Request $request)
{
$validated = $request->validate([
'department_id' => 'required|exists:departments,id',
'name' => 'required|string|max:255',
]);
Position::create($validated);
return redirect()->route('admin.positions.index')
->with('success', 'Posisi/Jabatan baru berhasil ditambahkan.');
}
public function show(Position $position)
{
// Hanya melempar object $department ke view tanpa load relasi positions
return view('pages.admin.positions.show', compact('position'));
}
public function edit(Position $position)
{
$departments = Department::orderBy('name', 'asc')->get();
return view('pages.admin.positions.edit', compact('position', 'departments'));
}
public function update(Request $request, Position $position)
{
$validated = $request->validate([
'department_id' => 'required|exists:departments,id',
'name' => 'required|string|max:255',
]);
$position->update($validated);
return redirect()->route('admin.positions.index')
->with('success', 'Data Posisi berhasil diperbarui.');
}
// Tambahkan ini di bagian fungsi-fungsi paling bawah
public function exportExcel()
{
$fileName = 'Master_Data_Jabatan_' . date('Ymd_Hi') . '.xlsx';
return \Maatwebsite\Excel\Facades\Excel::download(new \App\Exports\PositionsExport, $fileName);
}
public function exportPdf()
{
$positions = \App\Models\Position::all();
$pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('pages.admin.positions.export-pdf', compact('positions'))
->setPaper('a4', 'portrait');
return $pdf->download('Master_Data_Jabatan_' . date('Ymd_Hi') . '.pdf');
}
public function destroy(Position $position)
{
$position->delete();
return redirect()->route('admin.positions.index')
->with('success', 'Posisi berhasil dihapus.');
}
}
@@ -0,0 +1,53 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\ExamResult;
use Illuminate\Http\Request;
class ReportController extends Controller
{
/**
* Menampilkan daftar Karyawan (Trainee) beserta ringkasan progres mereka.
*/
public function training()
{
// Ambil semua user dengan role 'trainee', beserta relasi hasil ujiannya
$trainees = User::whereHas('roles', function ($query) {
$query->where('name', 'trainee');
})
->with('examResults') // Pastikan relasi examResults sudah dibuat di model User
->latest()
->paginate(15);
return view('pages.admin.reports.training', compact('trainees'));
}
/**
* Menampilkan detail riwayat training & ujian per individu.
*/
public function showTraining(User $user)
{
// Pastikan user tersebut benar-benar trainee
if (!$user->hasRole('trainee')) {
abort(404, 'Data riwayat training tidak ditemukan atau user bukan karyawan.');
}
// Ambil riwayat ujian, relasikan ke data Soal/Sesi (Exam) dan Dokumen (SOP)
$examHistory = ExamResult::where('user_id', $user->id)
->latest()
->get();
// Kalkulasi Statistik Cepat
$stats = [
'total_exams' => $examHistory->count(),
'passed' => $examHistory->where('is_passed', true)->count(),
'failed' => $examHistory->where('is_passed', false)->count(),
'avg_score' => $examHistory->avg('score') ?? 0,
];
return view('pages.admin.reports.show', compact('user', 'examHistory', 'stats'));
}
}
@@ -0,0 +1,36 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Sop;
use Illuminate\Http\Request;
class SopController extends Controller
{
public function index()
{
$sops = Sop::orderBy('sop_code', 'asc')->paginate(15);
return view('pages.admin.sop.index', compact('sops'));
}
public function create()
{
return view('pages.admin.sop.create');
}
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'sop_code' => 'required|unique:sops,sop_code',
'category_name' => 'nullable|string',
'version' => 'required|string',
'status' => 'required|in:Draft,Active,Obsolete',
]);
Sop::create($validated);
return redirect()->route('admin.sops.index')->with('success', 'Dokumen SOP berhasil didaftarkan.');
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
class VerifyEmailController extends Controller
{
/**
* Mark the authenticated user's email address as verified.
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
}
@@ -0,0 +1,34 @@
<?php
namespace App\Http\Controllers\Cbt;
use App\Http\Controllers\Controller;
use App\Models\Exam;
use App\Models\ExamResult;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class ExamSessionController extends Controller
{
/**
* Menampilkan halaman persiapan/ruang ujian
*/
public function show(Exam $exam)
{
$user = Auth::user();
// 1. Validasi: Cek apakah user sudah pernah lulus ujian ini
$existingResult = ExamResult::where('user_id', $user->id)
->where('exam_id', $exam->id)
->orderBy('created_at', 'desc')
->first();
if ($existingResult && $existingResult->is_passed) {
return redirect()->route('cbt.dashboard')
->with('error', 'Anda sudah pernah lulus ujian ini. Tidak perlu mengulang.');
}
// 2. Jika aman, arahkan ke View Ruang Ujian yang memuat komponen Livewire
return view('pages.cbt.exam-room', compact('exam'));
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Http\Controllers\Cbt;
use App\Http\Controllers\Controller;
use App\Models\Exam;
use App\Models\ExamResult;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class UserDashboardController extends Controller
{
/**
* Menampilkan Dashboard Portal Ujian Karyawan
*/
public function index()
{
// Ambil data user yang sedang login
$user = Auth::user();
// Tarik riwayat ujian yang pernah diikuti karyawan ini beserta detail ujiannya
$examHistory = ExamResult::where('user_id', $user->id)
->with('exam')
->orderBy('created_at', 'desc')
->get();
// Tarik daftar ujian yang tersedia (Bisa ditambahkan filter berdasarkan departemen/jabatan nanti)
$availableExams = Exam::orderBy('created_at', 'desc')->get();
// Kirim data ke view dashboard CBT
return view('pages.cbt.dashboard.dashboard', compact('user', 'examHistory', 'availableExams'));
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class ForcePasswordChange
{
public function handle(Request $request, Closure $next): Response
{
// Cek jika user login DAN wajib ganti password
if (Auth::check() && Auth::user()->must_change_password) {
// Pengecualian agar tidak terjebak dalam loop:
// Izinkan akses ke rute ganti password dan rute logout
$allowedRoutes = ['password.force-change', 'password.force-update', 'logout'];
if (!in_array($request->route()->getName(), $allowedRoutes)) {
return redirect()->route('password.force-change');
}
}
return $next($request);
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace App\Imports;
use App\Models\User;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\ToCollection;
use Maatwebsite\Excel\Concerns\WithHeadingRow;
use Illuminate\Support\Facades\Hash;
class EmployeesImport implements ToCollection, WithHeadingRow
{
public $successCount = 0;
public $failCount = 0;
/**
* Asumsi Header Excel (Baris ke-1):
* nik | nama_depan | nama_belakang | email | telepon | jenis_kelamin | department_id | position_id
*/
public function collection(Collection $rows)
{
foreach ($rows as $row) {
try {
// Validasi: Abaikan baris jika NIK, Nama Depan, atau Email kosong
if (empty($row['nik']) || empty($row['nama_depan']) || empty($row['email'])) {
$this->failCount++;
continue;
}
// Update jika NIK sudah ada, Create jika belum ada
$user = User::updateOrCreate(
['nik' => $row['nik']], // Pencarian berdasarkan NIK
[
'first_name' => $row['nama_depan'],
'last_name' => $row['nama_belakang'] ?? null,
'email' => $row['email'],
'phone' => $row['telepon'] ?? null,
'gender' => in_array(strtoupper($row['jenis_kelamin'] ?? ''), ['L', 'P']) ? strtoupper($row['jenis_kelamin']) : 'L',
'department_id' => $row['department_id'] ?? 1, // Fallback ID 1 jika kosong
'position_id' => $row['position_id'] ?? 1,
'password' => Hash::make('Karyawan123!'),
'is_active' => true,
'must_change_password' => true,
]
);
// Otomatis jadikan Trainee
if (!$user->hasAnyRole(['admin', 'trainer', 'trainee'])) {
$user->assignRole('trainee');
}
$this->successCount++;
} catch (\Exception $e) {
// Jika error (misal format email salah atau duplikat), masukkan ke hitungan gagal
$this->failCount++;
}
}
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\Livewire\Actions;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
class Logout
{
/**
* Log the current user out of the application.
*/
public function __invoke(): void
{
Auth::guard('web')->logout();
Session::invalidate();
Session::regenerateToken();
}
}
+89
View File
@@ -0,0 +1,89 @@
<?php
namespace App\Livewire\Forms;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Validate;
use Livewire\Form;
class LoginForm extends Form
{
#[Validate('required|string|email')]
public string $email = '';
#[Validate('required|string')]
public string $password = '';
#[Validate('boolean')]
public bool $remember = false;
/**
* Attempt to authenticate the request's credentials.
*
* @throws ValidationException
*/
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
// 1. Jalankan proses pencocokan email & password
if (! Auth::attempt($this->only(['email', 'password']), $this->remember)) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'form.email' => trans('auth.failed'),
]);
}
// 2. Ambil data user yang berhasil login
$user = Auth::user();
// 3. VALIDASI RBAC: Pastikan user memiliki minimal satu role aktif di database
// Ini mencegah user tanpa role (data menggantung) bisa masuk ke sistem
if ($user->roles()->count() === 0) {
// Jika tidak punya role, paksa logout demi keamanan
Auth::logout();
request()->session()->invalidate();
request()->session()->regenerateToken();
throw ValidationException::withMessages([
'form.email' => 'Akun Anda aktif, namun belum dikonfigurasi memiliki Role (Akses) oleh Admin. Silakan hubungi IT/HRD.',
]);
}
RateLimiter::clear($this->throttleKey());
}
/**
* Ensure the authentication request is not rate limited.
*/
protected function ensureIsNotRateLimited(): void
{
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}
event(new Lockout(request()));
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'form.email' => trans('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
]);
}
/**
* Get the authentication rate limiting throttle key.
*/
protected function throttleKey(): string
{
return Str::transliterate(Str::lower($this->email).'|'.request()->ip());
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace App\Livewire\Matrix;
use Livewire\Component;
use App\Models\User;
use App\Models\ExamResult;
class ComplianceMonitor extends Component
{
public function render()
{
// Contoh logika sederhana: Menghitung persentase karyawan yang sudah lulus ujian
$totalKaryawan = User::where('role', 'karyawan')->count();
// Hitung karyawan unik yang sudah memiliki minimal 1 hasil lulus
$karyawanLulus = ExamResult::where('is_passed', true)
->distinct('user_id')
->count('user_id');
$complianceRate = $totalKaryawan > 0 ? round(($karyawanLulus / $totalKaryawan) * 100) : 0;
return view('livewire.matrix.compliance-monitor', [
'complianceRate' => $complianceRate,
'totalKaryawan' => $totalKaryawan,
'karyawanLulus' => $karyawanLulus
]);
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Department extends Model
{
protected $fillable = ['name', 'code'];
// public function users(): HasMany
// {
// return $this->hasMany(User::class);
// }
// Relasi ke Posisi (dari tabel class_sections lama)
public function positions()
{
return $this->belongsToMany(Position::class, 'department_position');
}
// Relasi ke Karyawan (Users) untuk menghitung total karyawan di departemen ini
public function users()
{
return $this->hasMany(User::class, 'department_id');
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Exam extends Model
{
protected $fillable = [
'old_id',
'title',
'duration',
'passing_percentage'
];
public function results(): HasMany
{
return $this->hasMany(ExamResult::class);
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ExamResult extends Model
{
protected $fillable = [
'user_id',
'exam_id',
'is_attempted',
'score',
'is_passed'
];
protected $casts = [
'is_attempted' => 'boolean',
'is_passed' => 'boolean',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function exam(): BelongsTo
{
return $this->belongsTo(Exam::class);
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Position extends Model
{
protected $fillable = ['name'];
public function users()
{
return $this->hasMany(User::class, 'position_id');
}
public function departments()
{
return $this->belongsToMany(Department::class, 'department_position');
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Question extends Model
{
// Pastikan ini sesuai dengan nama tabel Anda di database (misalnya 'questions')
protected $table = 'questions';
// Sesuaikan array ini dengan nama kolom yang ada di database Anda
protected $fillable = [
'question_text',
'category',
'type',
'answer_a',
'answer_b',
'answer_c',
'answer_d',
'correct_answer'
];
}
+65
View File
@@ -0,0 +1,65 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class QuestionBank extends Model
{
// Opsional, tapi sangat disarankan agar Laravel tidak salah menebak nama tabel
protected $table = 'question_banks';
protected $fillable = [
// Atribut Soal dari desain Anda
'old_id',
'subject',
'question_type',
'question_level',
'passing_grade',
'duration',
'question_text',
'option_a',
'option_b',
'option_c',
'option_d',
'option_e',
'correct_answer',
// PENTING: Atribut Relasi yang dibutuhkan form Create/Edit & Filter Index
'department_id',
'position_id',
'training_matrix_id',
'created_by'
];
protected $casts = [
'correct_answer' => 'array', // Otomatis ubah JSON di database jadi Array di PHP
];
/*
|--------------------------------------------------------------------------
| RELASI DATABASE (Diperlukan agar Filter Dropdown & Tabel berfungsi)
|--------------------------------------------------------------------------
*/
public function department()
{
return $this->belongsTo(Department::class, 'department_id');
}
public function position()
{
return $this->belongsTo(Position::class, 'position_id');
}
public function matrix()
{
return $this->belongsTo(TrainingMatrix::class, 'training_matrix_id');
}
public function creator()
{
// Merujuk ke tabel users untuk mengetahui siapa staff/admin yang buat soal
return $this->belongsTo(User::class, 'created_by');
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
protected $fillable = ['name'];
public function users()
{
return $this->belongsToMany(User::class);
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
// 1. Baris Spatie di-comment/dihapus
// use Spatie\Activitylog\Traits\LogsActivity;
// use Spatie\Activitylog\LogOptions;
class Sop extends Model
{
// 2. Trait LogsActivity di-comment/dihapus
// use LogsActivity;
protected $table = 'sops';
protected $fillable = [
'title',
'sop_code',
'category_name',
'version',
'revision_history',
'status'
];
protected $casts = [
'revision_history' => 'array',
];
// 3. Fungsi getActivitylogOptions di-comment/dihapus
/*
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logFillable()
->logOnlyDirty()
->dontSubmitEmptyLogs()
->setDescriptionForEvent(fn(string $eventName) => "SOP {$eventName}");
}
*/
public function trainingMatrices(): HasMany
{
return $this->hasMany(TrainingMatrix::class, 'sop_id');
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class TrainingMatrix extends Model
{
protected $table = 'training_matrices';
protected $fillable = [
'department_id',
'position_id',
'sop_id'
];
public function department(): BelongsTo
{
return $this->belongsTo(Department::class);
}
public function position(): BelongsTo
{
return $this->belongsTo(Position::class);
}
public function sop(): BelongsTo
{
return $this->belongsTo(Sop::class);
}
}
+83
View File
@@ -0,0 +1,83 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
{
use HasFactory, Notifiable, HasRoles;
/**
* The attributes that are mass assignable.
* Kolom-kolom ini wajib didaftarkan agar form Create & Edit tidak diblokir Laravel.
*/
protected $fillable = [
'nik',
'identity_number',
'initial',
'first_name',
'last_name',
'phone',
'email',
'password',
'role',
'gender',
'date_of_birth',
'join_date',
'photo',
'department_id',
'position_id',
'training_matrix_id',
'old_id',
'old_password_hash',
'is_active',
];
/**
* The attributes that should be hidden for serialization.
*/
protected $hidden = [
'password',
'remember_token',
'old_password_hash',
];
/**
* Mengubah format string dari database menjadi format Date/Time Carbon otomatis.
*/
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'date_of_birth' => 'date',
'join_date' => 'date',
'is_active' => 'boolean',
];
// =========================================================================
// RELASI ANTAR TABEL (MASTER DATA)
// =========================================================================
// Relasi ke tabel departments
public function department()
{
return $this->belongsTo(Department::class, 'department_id');
}
// Relasi ke tabel positions
public function position()
{
return $this->belongsTo(Position::class, 'position_id');
}
// Jika model TrainingMatrix sudah Anda buat, buka komentar di bawah ini:
/*
public function trainingMatrix()
{
return $this->belongsTo(TrainingMatrix::class);
}
*/
}
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class UserDocument extends Model
{
protected $fillable = [
'user_id',
'document_name',
'file_path',
'file_type'
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Storage;
use Illuminate\Filesystem\FilesystemAdapter;
use League\Flysystem\Filesystem;
use League\Flysystem\WebDAV\WebDAVAdapter;
use Sabre\DAV\Client;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
// Mencatat setiap kali user Login
Event::listen(function (Login $event) {
activity('autentikasi')
->causedBy($event->user)
->log('User berhasil login ke dalam sistem');
});
// Mencatat setiap kali user Logout
Event::listen(function (Logout $event) {
activity('autentikasi')
->causedBy($event->user)
->log('User keluar (logout) dari sistem');
});
// Mendaftarkan Custom Driver Nextcloud (WebDAV)
Storage::extend('webdav', function ($app, $config) {
$client = new Client([
'baseUri' => $config['baseUri'],
'userName' => $config['userName'],
'password' => $config['password'],
]);
$adapter = new WebDAVAdapter($client, $config['pathPrefix'] ?? '');
$driver = new Filesystem($adapter);
return new FilesystemAdapter($driver, $adapter, $config);
});
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Livewire\Volt\Volt;
class VoltServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
//
}
/**
* Bootstrap services.
*/
public function boot(): void
{
Volt::mount([
config('livewire.view_path', resource_path('views/livewire')),
resource_path('views/pages'),
]);
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
use Illuminate\View\View;
class AppLayout extends Component
{
/**
* Get the view / contents that represents the component.
*/
public function render(): View
{
return view('layouts.app');
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
use Illuminate\View\View;
class GuestLayout extends Component
{
/**
* Get the view / contents that represents the component.
*/
public function render(): View
{
return view('layouts.guest');
}
}
Executable
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env php
<?php
use Illuminate\Foundation\Application;
use Symfony\Component\Console\Input\ArgvInput;
define('LARAVEL_START', microtime(true));
// Register the Composer autoloader...
require __DIR__.'/vendor/autoload.php';
// Bootstrap Laravel and handle the command...
/** @var Application $app */
$app = require_once __DIR__.'/bootstrap/app.php';
$status = $app->handleCommand(new ArgvInput);
exit($status);
+35
View File
@@ -0,0 +1,35 @@
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
// 1. Mendaftarkan Alias Middleware (Best Practice)
$middleware->alias([
// Alias untuk proteksi ganti password
'force.password' => \App\Http\Middleware\ForcePasswordChange::class,
// Alias wajib untuk Spatie Permission
'role' => \Spatie\Permission\Middleware\RoleMiddleware::class,
'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class,
'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class,
]);
// 2. Mendaftarkan Middleware Global ke grup 'web'
// Penulisan ini lebih rapi dan merupakan standar baru Laravel
$middleware->web(append: [
\App\Http\Middleware\ForcePasswordChange::class,
]);
})
->withExceptions(function (Exceptions $exceptions): void {
//
})->create();
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+6
View File
@@ -0,0 +1,6 @@
<?php
return [
App\Providers\AppServiceProvider::class,
App\Providers\VoltServiceProvider::class,
];
+94
View File
@@ -0,0 +1,94 @@
{
"$schema": "https://getcomposer.org/schema.json",
"name": "laravel/laravel",
"type": "project",
"description": "The skeleton application for the Laravel framework.",
"keywords": ["laravel", "framework"],
"license": "MIT",
"require": {
"php": "^8.3",
"barryvdh/laravel-dompdf": "^3.1",
"laravel/framework": "^13.8",
"laravel/tinker": "^3.0",
"league/flysystem-webdav": "^3.31",
"livewire/livewire": "^3.6.4",
"livewire/volt": "^1.7.0",
"maatwebsite/excel": "^3.1",
"spatie/laravel-activitylog": "^5.0",
"spatie/laravel-permission": "^7.4"
},
"require-dev": {
"fakerphp/faker": "^1.23",
"laravel/breeze": "^2.4",
"laravel/pail": "^1.2.5",
"laravel/pao": "^1.0.6",
"laravel/pint": "^1.27",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"phpunit/phpunit": "^12.5.12"
},
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"setup": [
"composer install",
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
"@php artisan key:generate",
"@php artisan migrate --force",
"npm install --ignore-scripts",
"npm run build"
],
"dev": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
],
"test": [
"@php artisan config:clear --ansi @no_additional_args",
"@php artisan test"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi",
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
"@php artisan migrate --graceful --ansi"
],
"pre-package-uninstall": [
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
]
},
"extra": {
"laravel": {
"dont-discover": []
}
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true,
"php-http/discovery": true
}
},
"minimum-stability": "stable",
"prefer-stable": true
}
Generated
+10329
View File
File diff suppressed because it is too large Load Diff
+126
View File
@@ -0,0 +1,126 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Application Name
|--------------------------------------------------------------------------
|
| This value is the name of your application, which will be used when the
| framework needs to place the application's name in a notification or
| other UI elements where an application name needs to be displayed.
|
*/
'name' => env('APP_NAME', 'Laravel'),
/*
|--------------------------------------------------------------------------
| Application Environment
|--------------------------------------------------------------------------
|
| This value determines the "environment" your application is currently
| running in. This may determine how you prefer to configure various
| services the application utilizes. Set this in your ".env" file.
|
*/
'env' => env('APP_ENV', 'production'),
/*
|--------------------------------------------------------------------------
| Application Debug Mode
|--------------------------------------------------------------------------
|
| When your application is in debug mode, detailed error messages with
| stack traces will be shown on every error that occurs within your
| application. If disabled, a simple generic error page is shown.
|
*/
'debug' => (bool) env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
|
| This URL is used by the console to properly generate URLs when using
| the Artisan command line tool. You should set this to the root of
| the application so that it's available within Artisan commands.
|
*/
'url' => env('APP_URL', 'http://localhost'),
/*
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. The timezone
| is set to "UTC" by default as it is suitable for most use cases.
|
*/
'timezone' => env('APP_TIMEZONE', 'Asia/Jakarta'),
/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by Laravel's translation / localization methods. This option can be
| set to any locale for which you plan to have translation strings.
|
*/
'locale' => env('APP_LOCALE', 'en'),
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is utilized by Laravel's encryption services and should be set
| to a random, 32 character string to ensure that all encrypted values
| are secure. You should do this prior to deploying the application.
|
*/
'cipher' => 'AES-256-CBC',
'key' => env('APP_KEY'),
'previous_keys' => [
...array_filter(
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
),
],
/*
|--------------------------------------------------------------------------
| Maintenance Mode Driver
|--------------------------------------------------------------------------
|
| These configuration options determine the driver used to determine and
| manage Laravel's "maintenance mode" status. The "cache" driver will
| allow maintenance mode to be controlled across multiple machines.
|
| Supported drivers: "file", "cache"
|
*/
'maintenance' => [
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
'store' => env('APP_MAINTENANCE_STORE', 'database'),
],
];
+117
View File
@@ -0,0 +1,117 @@
<?php
use App\Models\User;
return [
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option defines the default authentication "guard" and password
| reset "broker" for your application. You may change these values
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [
'guard' => env('AUTH_GUARD', 'web'),
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| which utilizes session storage plus the Eloquent user provider.
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| Supported: "session"
|
*/
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| If you have multiple user tables or models you may configure multiple
| providers to represent the model / table. These providers may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => env('AUTH_MODEL', User::class),
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
/*
|--------------------------------------------------------------------------
| Resetting Passwords
|--------------------------------------------------------------------------
|
| These configuration options specify the behavior of Laravel's password
| reset functionality, including the table utilized for token storage
| and the user provider that is invoked to actually retrieve users.
|
| The expiry time is the number of minutes that each reset token will be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
| The throttle setting is the number of seconds a user must wait before
| generating more password reset tokens. This prevents the user from
| quickly generating a very large amount of password reset tokens.
|
*/
'passwords' => [
'users' => [
'provider' => 'users',
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
'expire' => 60,
'throttle' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Password Confirmation Timeout
|--------------------------------------------------------------------------
|
| Here you may define the number of seconds before a password confirmation
| window expires and users are asked to re-enter their password via the
| confirmation screen. By default, the timeout lasts for three hours.
|
*/
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
];
+136
View File
@@ -0,0 +1,136 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Cache Store
|--------------------------------------------------------------------------
|
| This option controls the default cache store that will be used by the
| framework. This connection is utilized if another isn't explicitly
| specified when running a cache operation inside the application.
|
*/
'default' => env('CACHE_STORE', 'database'),
/*
|--------------------------------------------------------------------------
| Cache Stores
|--------------------------------------------------------------------------
|
| Here you may define all of the cache "stores" for your application as
| well as their drivers. You may even define multiple stores for the
| same cache driver to group types of items stored in your caches.
|
| Supported drivers: "array", "database", "file", "memcached",
| "redis", "dynamodb", "storage", "octane",
| "session", "failover", "null"
|
*/
'stores' => [
'array' => [
'driver' => 'array',
'serialize' => false,
],
'database' => [
'driver' => 'database',
'connection' => env('DB_CACHE_CONNECTION'),
'table' => env('DB_CACHE_TABLE', 'cache'),
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
],
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache/data'),
'lock_path' => storage_path('framework/cache/data'),
],
'storage' => [
'driver' => 'storage',
'disk' => env('CACHE_STORAGE_DISK'),
'path' => env('CACHE_STORAGE_PATH', 'framework/cache/data'),
],
'memcached' => [
'driver' => 'memcached',
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
'sasl' => [
env('MEMCACHED_USERNAME'),
env('MEMCACHED_PASSWORD'),
],
'options' => [
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
],
'servers' => [
[
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
'port' => env('MEMCACHED_PORT', 11211),
'weight' => 100,
],
],
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
],
'dynamodb' => [
'driver' => 'dynamodb',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
'endpoint' => env('DYNAMODB_ENDPOINT'),
],
'octane' => [
'driver' => 'octane',
],
'failover' => [
'driver' => 'failover',
'stores' => [
'database',
'array',
],
],
],
/*
|--------------------------------------------------------------------------
| Cache Key Prefix
|--------------------------------------------------------------------------
|
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
| stores, there might be other applications using the same cache. For
| that reason, you may prefix every cache key to avoid collisions.
|
*/
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
/*
|--------------------------------------------------------------------------
| Serializable Classes
|--------------------------------------------------------------------------
|
| This value determines the classes that can be unserialized from cache
| storage. By default, no PHP classes will be unserialized from your
| cache to prevent gadget chain attacks if your APP_KEY is leaked.
|
*/
'serializable_classes' => false,
];
+200
View File
@@ -0,0 +1,200 @@
<?php
use Illuminate\Support\Str;
use Pdo\Mysql;
return [
/*
|--------------------------------------------------------------------------
| Default Database Connection Name
|--------------------------------------------------------------------------
|
| Here you may specify which of the database connections below you wish
| to use as your default connection for database operations. This is
| the connection which will be utilized unless another connection
| is explicitly specified when you execute a query / statement.
|
*/
'default' => env('DB_CONNECTION', 'sqlite'),
/*
|--------------------------------------------------------------------------
| Database Connections
|--------------------------------------------------------------------------
|
| Below are all of the database connections defined for your application.
| An example configuration is provided for each database system which
| is supported by Laravel. You're free to add / remove connections.
|
*/
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DB_URL'),
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
'busy_timeout' => null,
'journal_mode' => null,
'synchronous' => null,
'transaction_mode' => 'DEFERRED',
],
'mysql' => [
'driver' => 'mysql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
// database lama dari CI3
'mysql_old' => [
'driver' => 'mysql',
'host' => env('DB_HOST_OLD', '127.0.0.1'),
'port' => env('DB_PORT_OLD', '3306'),
'database' => env('DB_DATABASE_OLD', 'lmsv2-old'),
'username' => env('DB_USERNAME_OLD', 'root'),
'password' => env('DB_PASSWORD_OLD', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
'engine' => null,
],
'mariadb' => [
'driver' => 'mariadb',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => env('DB_SSLMODE', 'prefer'),
],
'sqlsrv' => [
'driver' => 'sqlsrv',
'url' => env('DB_URL'),
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', '1433'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
],
],
/*
|--------------------------------------------------------------------------
| Migration Repository Table
|--------------------------------------------------------------------------
|
| This table keeps track of all the migrations that have already run for
| your application. Using this information, we can determine which of
| the migrations on disk haven't actually been run on the database.
|
*/
'migrations' => [
'table' => 'migrations',
'update_date_on_publish' => true,
],
/*
|--------------------------------------------------------------------------
| Redis Databases
|--------------------------------------------------------------------------
|
| Redis is an open source, fast, and advanced key-value store that also
| provides a richer body of commands than a typical key-value system
| such as Memcached. You may define your connection settings here.
|
*/
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
'persistent' => env('REDIS_PERSISTENT', false),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
'max_retries' => env('REDIS_MAX_RETRIES', 3),
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
'max_retries' => env('REDIS_MAX_RETRIES', 3),
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
],
],
];
+88
View File
@@ -0,0 +1,88 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Filesystem Disk
|--------------------------------------------------------------------------
|
| Here you may specify the default filesystem disk that should be used
| by the framework. The "local" disk, as well as a variety of cloud
| based disks are available to your application for file storage.
|
*/
'default' => env('FILESYSTEM_DISK', 'local'),
/*
|--------------------------------------------------------------------------
| Filesystem Disks
|--------------------------------------------------------------------------
|
| Below you may configure as many filesystem disks as necessary, and you
| may even configure multiple disks for the same driver. Examples for
| most supported storage drivers are configured here for reference.
|
| Supported drivers: "local", "ftp", "sftp", "s3"
|
*/
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app/private'),
'serve' => true,
'throw' => false,
'report' => false,
],
'nextcloud' => [
'driver' => 'webdav',
'baseUri' => env('NEXTCLOUD_BASE_URI'),
'userName' => env('NEXTCLOUD_USERNAME'),
'password' => env('NEXTCLOUD_PASSWORD'),
'pathPrefix' => env('NEXTCLOUD_PATH_PREFIX', '/remote.php/webdav/'),
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
'visibility' => 'public',
'throw' => false,
'report' => false,
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
'report' => false,
],
],
/*
|--------------------------------------------------------------------------
| Symbolic Links
|--------------------------------------------------------------------------
|
| Here you may configure the symbolic links that will be created when the
| `storage:link` Artisan command is executed. The array keys should be
| the locations of the links and the values should be their targets.
|
*/
'links' => [
public_path('storage') => storage_path('app/public'),
],
];
+132
View File
@@ -0,0 +1,132 @@
<?php
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;
use Monolog\Processor\PsrLogMessageProcessor;
return [
/*
|--------------------------------------------------------------------------
| Default Log Channel
|--------------------------------------------------------------------------
|
| This option defines the default log channel that is utilized to write
| messages to your logs. The value provided here should match one of
| the channels present in the list of "channels" configured below.
|
*/
'default' => env('LOG_CHANNEL', 'stack'),
/*
|--------------------------------------------------------------------------
| Deprecations Log Channel
|--------------------------------------------------------------------------
|
| This option controls the log channel that should be used to log warnings
| regarding deprecated PHP and library features. This allows you to get
| your application ready for upcoming major versions of dependencies.
|
*/
'deprecations' => [
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
],
/*
|--------------------------------------------------------------------------
| Log Channels
|--------------------------------------------------------------------------
|
| Here you may configure the log channels for your application. Laravel
| utilizes the Monolog PHP logging library, which includes a variety
| of powerful log handlers and formatters that you're free to use.
|
| Available drivers: "single", "daily", "slack", "syslog",
| "errorlog", "monolog", "custom", "stack"
|
*/
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
'ignore_exceptions' => false,
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => env('LOG_DAILY_DAYS', 14),
'replace_placeholders' => true,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')),
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
'level' => env('LOG_LEVEL', 'critical'),
'replace_placeholders' => true,
],
'papertrail' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
'handler_with' => [
'host' => env('PAPERTRAIL_URL'),
'port' => env('PAPERTRAIL_PORT'),
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
],
'processors' => [PsrLogMessageProcessor::class],
],
'stderr' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => StreamHandler::class,
'handler_with' => [
'stream' => 'php://stderr',
],
'formatter' => env('LOG_STDERR_FORMATTER'),
'processors' => [PsrLogMessageProcessor::class],
],
'syslog' => [
'driver' => 'syslog',
'level' => env('LOG_LEVEL', 'debug'),
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
'replace_placeholders' => true,
],
'errorlog' => [
'driver' => 'errorlog',
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
],
'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
],
];
+118
View File
@@ -0,0 +1,118 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Mailer
|--------------------------------------------------------------------------
|
| This option controls the default mailer that is used to send all email
| messages unless another mailer is explicitly specified when sending
| the message. All additional mailers can be configured within the
| "mailers" array. Examples of each type of mailer are provided.
|
*/
'default' => env('MAIL_MAILER', 'log'),
/*
|--------------------------------------------------------------------------
| Mailer Configurations
|--------------------------------------------------------------------------
|
| Here you may configure all of the mailers used by your application plus
| their respective settings. Several examples have been configured for
| you and you are free to add your own as your application requires.
|
| Laravel supports a variety of mail "transport" drivers that can be used
| when delivering an email. You may specify which one you're using for
| your mailers below. You may also add additional mailers if needed.
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
| "postmark", "resend", "log", "array",
| "failover", "roundrobin"
|
*/
'mailers' => [
'smtp' => [
'transport' => 'smtp',
'scheme' => env('MAIL_SCHEME'),
'url' => env('MAIL_URL'),
'host' => env('MAIL_HOST', '127.0.0.1'),
'port' => env('MAIL_PORT', 2525),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
],
'ses' => [
'transport' => 'ses',
],
'postmark' => [
'transport' => 'postmark',
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
// 'client' => [
// 'timeout' => 5,
// ],
],
'resend' => [
'transport' => 'resend',
],
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
],
'log' => [
'transport' => 'log',
'channel' => env('MAIL_LOG_CHANNEL'),
],
'array' => [
'transport' => 'array',
],
'failover' => [
'transport' => 'failover',
'mailers' => [
'smtp',
'log',
],
'retry_after' => 60,
],
'roundrobin' => [
'transport' => 'roundrobin',
'mailers' => [
'ses',
'postmark',
],
'retry_after' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Global "From" Address
|--------------------------------------------------------------------------
|
| You may wish for all emails sent by your application to be sent from
| the same address. Here you may specify a name and address that is
| used globally for all emails that are sent by your application.
|
*/
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')),
],
];
+129
View File
@@ -0,0 +1,129 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Queue Connection Name
|--------------------------------------------------------------------------
|
| Laravel's queue supports a variety of backends via a single, unified
| API, giving you convenient access to each backend using identical
| syntax for each. The default queue connection is defined below.
|
*/
'default' => env('QUEUE_CONNECTION', 'database'),
/*
|--------------------------------------------------------------------------
| Queue Connections
|--------------------------------------------------------------------------
|
| Here you may configure the connection options for every queue backend
| used by your application. An example configuration is provided for
| each backend supported by Laravel. You're also free to add more.
|
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
| "deferred", "background", "failover", "null"
|
*/
'connections' => [
'sync' => [
'driver' => 'sync',
],
'database' => [
'driver' => 'database',
'connection' => env('DB_QUEUE_CONNECTION'),
'table' => env('DB_QUEUE_TABLE', 'jobs'),
'queue' => env('DB_QUEUE', 'default'),
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
'after_commit' => false,
],
'beanstalkd' => [
'driver' => 'beanstalkd',
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
'queue' => env('BEANSTALKD_QUEUE', 'default'),
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
'block_for' => 0,
'after_commit' => false,
],
'sqs' => [
'driver' => 'sqs',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
'queue' => env('SQS_QUEUE', 'default'),
'suffix' => env('SQS_SUFFIX'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'after_commit' => false,
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
'block_for' => null,
'after_commit' => false,
],
'deferred' => [
'driver' => 'deferred',
],
'background' => [
'driver' => 'background',
],
'failover' => [
'driver' => 'failover',
'connections' => [
'database',
'deferred',
],
],
],
/*
|--------------------------------------------------------------------------
| Job Batching
|--------------------------------------------------------------------------
|
| The following options configure the database and table that store job
| batching information. These options can be updated to any database
| connection and table which has been defined by your application.
|
*/
'batching' => [
'database' => env('DB_CONNECTION', 'sqlite'),
'table' => 'job_batches',
],
/*
|--------------------------------------------------------------------------
| Failed Queue Jobs
|--------------------------------------------------------------------------
|
| These options configure the behavior of failed queue job logging so you
| can control how and where failed jobs are stored. Laravel ships with
| support for storing failed jobs in a simple file or in a database.
|
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
*/
'failed' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
'database' => env('DB_CONNECTION', 'sqlite'),
'table' => 'failed_jobs',
],
];
+38
View File
@@ -0,0 +1,38 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Third Party Services
|--------------------------------------------------------------------------
|
| This file is for storing the credentials for third party services such
| as Mailgun, Postmark, AWS and more. This file provides the de facto
| location for this type of information, allowing packages to have
| a conventional file to locate the various service credentials.
|
*/
'postmark' => [
'key' => env('POSTMARK_API_KEY'),
],
'resend' => [
'key' => env('RESEND_API_KEY'),
],
'ses' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
'slack' => [
'notifications' => [
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
],
],
];
+233
View File
@@ -0,0 +1,233 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Session Driver
|--------------------------------------------------------------------------
|
| This option determines the default session driver that is utilized for
| incoming requests. Laravel supports a variety of storage options to
| persist session data. Database storage is a great default choice.
|
| Supported: "file", "cookie", "database", "memcached",
| "redis", "dynamodb", "array"
|
*/
'driver' => env('SESSION_DRIVER', 'database'),
/*
|--------------------------------------------------------------------------
| Session Lifetime
|--------------------------------------------------------------------------
|
| Here you may specify the number of minutes that you wish the session
| to be allowed to remain idle before it expires. If you want them
| to expire immediately when the browser is closed then you may
| indicate that via the expire_on_close configuration option.
|
*/
'lifetime' => (int) env('SESSION_LIFETIME', 120),
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
/*
|--------------------------------------------------------------------------
| Session Encryption
|--------------------------------------------------------------------------
|
| This option allows you to easily specify that all of your session data
| should be encrypted before it's stored. All encryption is performed
| automatically by Laravel and you may use the session like normal.
|
*/
'encrypt' => env('SESSION_ENCRYPT', false),
/*
|--------------------------------------------------------------------------
| Session File Location
|--------------------------------------------------------------------------
|
| When utilizing the "file" session driver, the session files are placed
| on disk. The default storage location is defined here; however, you
| are free to provide another location where they should be stored.
|
*/
'files' => storage_path('framework/sessions'),
/*
|--------------------------------------------------------------------------
| Session Database Connection
|--------------------------------------------------------------------------
|
| When using the "database" or "redis" session drivers, you may specify a
| connection that should be used to manage these sessions. This should
| correspond to a connection in your database configuration options.
|
*/
'connection' => env('SESSION_CONNECTION'),
/*
|--------------------------------------------------------------------------
| Session Database Table
|--------------------------------------------------------------------------
|
| When using the "database" session driver, you may specify the table to
| be used to store sessions. Of course, a sensible default is defined
| for you; however, you're welcome to change this to another table.
|
*/
'table' => env('SESSION_TABLE', 'sessions'),
/*
|--------------------------------------------------------------------------
| Session Cache Store
|--------------------------------------------------------------------------
|
| When using one of the framework's cache driven session backends, you may
| define the cache store which should be used to store the session data
| between requests. This must match one of your defined cache stores.
|
| Affects: "dynamodb", "memcached", "redis"
|
*/
'store' => env('SESSION_STORE'),
/*
|--------------------------------------------------------------------------
| Session Sweeping Lottery
|--------------------------------------------------------------------------
|
| Some session drivers must manually sweep their storage location to get
| rid of old sessions from storage. Here are the chances that it will
| happen on a given request. By default, the odds are 2 out of 100.
|
*/
'lottery' => [2, 100],
/*
|--------------------------------------------------------------------------
| Session Cookie Name
|--------------------------------------------------------------------------
|
| Here you may change the name of the session cookie that is created by
| the framework. Typically, you should not need to change this value
| since doing so does not grant a meaningful security improvement.
|
*/
'cookie' => env(
'SESSION_COOKIE',
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
),
/*
|--------------------------------------------------------------------------
| Session Cookie Path
|--------------------------------------------------------------------------
|
| The session cookie path determines the path for which the cookie will
| be regarded as available. Typically, this will be the root path of
| your application, but you're free to change this when necessary.
|
*/
'path' => env('SESSION_PATH', '/'),
/*
|--------------------------------------------------------------------------
| Session Cookie Domain
|--------------------------------------------------------------------------
|
| This value determines the domain and subdomains the session cookie is
| available to. By default, the cookie will be available to the root
| domain without subdomains. Typically, this shouldn't be changed.
|
*/
'domain' => env('SESSION_DOMAIN'),
/*
|--------------------------------------------------------------------------
| HTTPS Only Cookies
|--------------------------------------------------------------------------
|
| By setting this option to true, session cookies will only be sent back
| to the server if the browser has a HTTPS connection. This will keep
| the cookie from being sent to you when it can't be done securely.
|
*/
'secure' => env('SESSION_SECURE_COOKIE'),
/*
|--------------------------------------------------------------------------
| HTTP Access Only
|--------------------------------------------------------------------------
|
| Setting this value to true will prevent JavaScript from accessing the
| value of the cookie and the cookie will only be accessible through
| the HTTP protocol. It's unlikely you should disable this option.
|
*/
'http_only' => env('SESSION_HTTP_ONLY', true),
/*
|--------------------------------------------------------------------------
| Same-Site Cookies
|--------------------------------------------------------------------------
|
| This option determines how your cookies behave when cross-site requests
| take place, and can be used to mitigate CSRF attacks. By default, we
| will set this value to "lax" to permit secure cross-site requests.
|
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
| Supported: "lax", "strict", "none", null
|
*/
'same_site' => env('SESSION_SAME_SITE', 'lax'),
/*
|--------------------------------------------------------------------------
| Partitioned Cookies
|--------------------------------------------------------------------------
|
| Setting this value to true will tie the cookie to the top-level site for
| a cross-site context. Partitioned cookies are accepted by the browser
| when flagged "secure" and the Same-Site attribute is set to "none".
|
*/
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
/*
|--------------------------------------------------------------------------
| Session Serialization
|--------------------------------------------------------------------------
|
| This value controls the serialization strategy for session data, which
| is JSON by default. Setting this to "php" allows the storage of PHP
| objects in the session but can make an application vulnerable to
| "gadget chain" serialization attacks if the APP_KEY is leaked.
|
| Supported: "json", "php"
|
*/
'serialization' => 'json',
];
+1
View File
@@ -0,0 +1 @@
*.sqlite*
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @extends Factory<User>
*/
class UserFactory extends Factory
{
/**
* The current password being used by the factory.
*/
protected static ?string $password;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
}
@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('cache', function (Blueprint $table) {
$table->string('key')->primary();
$table->mediumText('value');
$table->bigInteger('expiration')->index();
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->bigInteger('expiration')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};
@@ -0,0 +1,59 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('jobs', function (Blueprint $table) {
$table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedSmallInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('connection');
$table->string('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
$table->index(['connection', 'queue', 'failed_at']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
Schema::dropIfExists('job_batches');
Schema::dropIfExists('failed_jobs');
}
};
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// create_departments_table.php
Schema::create('departments', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('code')->unique();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('departments');
}
};
@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('question_banks', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('old_id')->nullable()->index(); // Kolom penaut data lama
$table->longText('question_text');
$table->longText('option_a')->nullable();
$table->longText('option_b')->nullable();
$table->longText('option_c')->nullable();
$table->longText('option_d')->nullable();
$table->longText('option_e')->nullable();
$table->text('correct_answer')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('question_banks');
}
};
@@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('exams', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('old_id')->nullable()->index(); // Kolom penaut data lama
$table->string('title');
$table->string('duration')->nullable(); // Dalam menit
$table->string('passing_percentage')->nullable(); // KKM Kelulusan
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('exams');
}
};
@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('positions', function (Blueprint $table) {
$table->id();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('positions');
}
};
@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('sops', function (Blueprint $table) {
$table->id(); // 1. id sop
$table->string('title'); // 2. judul
$table->string('sop_code')->unique(); // 3. kode_sop
$table->string('category_name')->nullable(); // 4. kategori name
$table->string('version')->default('1.0'); // 5. versi
$table->text('revision_history')->nullable(); // 6. historical rev sop (bisa diset text atau json)
$table->enum('status', ['Draft', 'Active', 'Obsolete'])->default('Active'); // 7. status sop
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('sops');
}
};
@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('training_matrices', function (Blueprint $table) {
$table->id();
// 1. Relasi ke Departemen
$table->foreignId('department_id')->constrained('departments')->cascadeOnDelete();
// 2. Relasi ke Posisi/Jabatan
$table->foreignId('position_id')->constrained('positions')->cascadeOnDelete();
// 3. Relasi ke SOP
$table->foreignId('sop_id')->constrained('sops')->cascadeOnDelete();
$table->timestamps();
// Opsional tapi sangat disarankan: Mencegah duplikasi data agar 1 departemen + 1 jabatan + 1 SOP yang sama tidak terinput 2 kali
$table->unique(['department_id', 'position_id', 'sop_id'], 'matrix_unique_combination');
});
}
public function down(): void
{
Schema::dropIfExists('training_matrices');
}
};
@@ -0,0 +1,79 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
// --- 1. Identitas Utama ---
$table->string('nik')->unique()->nullable(); // NIK / Nomor Karyawan
$table->string('identity_number')->nullable(); // No. KTP / NPWP
// --- 2. Penamaan ---
$table->string('initial', 10)->nullable();
$table->string('first_name');
$table->string('last_name')->nullable();
$table->string('phone')->nullable();
// --- 3. Autentikasi & Hak Akses ---
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->string('role')->default('karyawan'); // Role: admin, trainer, karyawan
// --- 4. Demografi & Status ---
$table->enum('gender', ['L', 'P'])->nullable(); // Laki-laki / Perempuan
$table->date('date_of_birth')->nullable();
$table->date('join_date')->nullable();
// --- 5. Media ---
$table->string('photo')->nullable(); // URL/Path foto profil
// --- 6. Relasi Master Data (Foreign Keys) ---
$table->foreignId('department_id')->nullable()->constrained('departments')->nullOnDelete();
$table->foreignId('position_id')->nullable()->constrained('positions')->nullOnDelete();
$table->foreignId('training_matrix_id')->nullable()->constrained('training_matrices')->nullOnDelete();
// --- 7. Kebutuhan Migrasi dari LMS CI3 (Legacy) ---
$table->unsignedBigInteger('old_id')->nullable(); // Menjaga relasi nilai ujian lama
$table->string('old_password_hash')->nullable(); // Backup password MD5/SHA1 lama
$table->rememberToken();
$table->timestamps();
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('sessions');
}
};
@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('user_documents', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); // Jika user dihapus, dokumen ikut terhapus
$table->string('document_name'); // Cth: "Ijazah S1", "Sertifikat CPOB"
$table->string('file_path'); // Lokasi file di server (storage/app/public/...)
$table->string('file_type')->nullable(); // Cth: pdf, png, jpg
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('user_documents');
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('exam_results', function (Blueprint $table) {
$table->id();
// Relasi ke User dan Exam baru
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->foreignId('exam_id')->constrained('exams')->cascadeOnDelete();
$table->integer('is_attempted')->default(0);
$table->decimal('score', 5, 2)->nullable();
$table->boolean('is_passed')->default(false);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('exam_results');
}
};
@@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('activity_log', function (Blueprint $table) {
$table->id();
$table->string('log_name')->nullable()->index();
$table->text('description');
$table->nullableMorphs('subject', 'subject');
$table->string('event')->nullable();
$table->nullableMorphs('causer', 'causer');
$table->json('attribute_changes')->nullable();
$table->json('properties')->nullable();
$table->timestamps();
});
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('question_banks', function (Blueprint $table) {
//
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('question_banks', function (Blueprint $table) {
//
});
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('roles', function (Blueprint $table) {
$table->id();
$table->string('name'); // Contoh: 'trainer', 'trainee'
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('roles');
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('role_user', function (Blueprint $table) {
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('role_id')->constrained()->onDelete('cascade');
$table->primary(['user_id', 'role_id']); // Mencegah duplikasi
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('role_user');
}
};
@@ -0,0 +1,122 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
$teams = config('permission.teams');
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
throw_if(empty($tableNames), 'Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), 'Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
// 1. Permissions Table
if (!Schema::hasTable($tableNames['permissions'])) {
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('guard_name');
$table->timestamps();
$table->unique(['name', 'guard_name']);
});
}
// 2. Roles Table
if (!Schema::hasTable($tableNames['roles'])) {
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
$table->id();
if ($teams || config('permission.testing')) {
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
}
$table->string('name');
$table->string('guard_name');
$table->timestamps();
if ($teams || config('permission.testing')) {
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
} else {
$table->unique(['name', 'guard_name']);
}
});
}
// 3. Model Has Permissions
if (!Schema::hasTable($tableNames['model_has_permissions'])) {
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
$table->unsignedBigInteger($pivotPermission);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
$table->foreign($pivotPermission)->references('id')->on($tableNames['permissions'])->cascadeOnDelete();
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_permission_model_type_primary');
} else {
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_permission_model_type_primary');
}
});
}
// 4. Model Has Roles
if (!Schema::hasTable($tableNames['model_has_roles'])) {
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
$table->unsignedBigInteger($pivotRole);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
$table->foreign($pivotRole)->references('id')->on($tableNames['roles'])->cascadeOnDelete();
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'], 'model_has_roles_role_model_type_primary');
} else {
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'], 'model_has_roles_role_model_type_primary');
}
});
}
// 5. Role Has Permissions
if (!Schema::hasTable($tableNames['role_has_permissions'])) {
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
$table->unsignedBigInteger($pivotPermission);
$table->unsignedBigInteger($pivotRole);
$table->foreign($pivotPermission)->references('id')->on($tableNames['permissions'])->cascadeOnDelete();
$table->foreign($pivotRole)->references('id')->on($tableNames['roles'])->cascadeOnDelete();
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
});
}
app('cache')->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)->forget(config('permission.cache.key'));
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$tableNames = config('permission.table_names');
throw_if(empty($tableNames), 'Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
Schema::dropIfExists($tableNames['role_has_permissions']);
Schema::dropIfExists($tableNames['model_has_roles']);
Schema::dropIfExists($tableNames['model_has_permissions']);
Schema::dropIfExists($tableNames['roles']);
Schema::dropIfExists($tableNames['permissions']);
}
};
@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
// Tambahkan kolom boolean, default-nya 'true' (harus ganti password)
$table->boolean('must_change_password')->default(true)->after('password');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('must_change_password');
});
}
};
@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('department_position', function (Blueprint $table) {
// Menghubungkan ID Departemen dan ID Posisi
$table->foreignId('department_id')->constrained('departments')->cascadeOnDelete();
$table->foreignId('position_id')->constrained('positions')->cascadeOnDelete();
// Mencegah data ganda
$table->unique(['department_id', 'position_id']);
});
}
public function down(): void
{
Schema::dropIfExists('department_position');
}
};
@@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_active')->default(true)->after('password');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('is_active');
});
}
};
@@ -0,0 +1,44 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('question_banks', function (Blueprint $table) {
$table->id();
$table->integer('old_id')->nullable(); // Untuk menyimpan ID dari database lama
$table->string('subject')->nullable();
$table->unsignedBigInteger('department_id')->nullable();
$table->unsignedBigInteger('position_id')->nullable();
$table->unsignedBigInteger('training_matrix_id')->nullable();
// Disesuaikan persis dengan Model
$table->string('question_type');
$table->string('question_level');
$table->integer('passing_grade')->default(0);
$table->integer('duration')->default(0);
$table->text('question_text')->nullable();
$table->text('option_a')->nullable();
$table->text('option_b')->nullable();
$table->text('option_c')->nullable();
$table->text('option_d')->nullable();
$table->text('option_e')->nullable();
// Kolom ini akan otomatis menjadi JSON di database karena cast 'array' di Model
$table->text('correct_answer')->nullable();
$table->unsignedBigInteger('created_by')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('question_banks');
}
};
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
use WithoutModelEvents;
/**
* Seed the application's database.
*/
public function run(): void
{
// User::factory(10)->create();
User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
]);
}
}
@@ -0,0 +1,42 @@
= [
{#8544
+"Field": "id",
+"Type": "int(11)",
+"Null": "NO",
+"Key": "PRI",
+"Default": null,
+"Extra": "auto_increment",
},
{#8542
+"Field": "section",
+"Type": "varchar(60)",
+"Null": "YES",
+"Key": "",
+"Default": null,
+"Extra": "",
},
{#8541
+"Field": "is_active",
+"Type": "varchar(255)",
+"Null": "YES",
+"Key": "",
+"Default": "no",
+"Extra": "",
},
{#8546
+"Field": "created_at",
+"Type": "timestamp",
+"Null": "NO",
+"Key": "",
+"Default": "current_timestamp()",
+"Extra": "",
},
{#8529
+"Field": "updated_at",
+"Type": "timestamp",
+"Null": "NO",
+"Key": "",
+"Default": "current_timestamp()",
+"Extra": "on update current_timestamp()",
},
]
+2660
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
{
"$schema": "https://www.schemastore.org/package.json",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.2",
"@tailwindcss/vite": "^4.0.0",
"autoprefixer": "^10.4.2",
"concurrently": "^9.0.1",
"laravel-vite-plugin": "^3.1",
"postcss": "^8.4.31",
"tailwindcss": "^3.1.0",
"vite": "^8.0.0"
}
}
+36
View File
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory>tests/Feature</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>app</directory>
</include>
</source>
<php>
<env name="APP_ENV" value="testing"/>
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="BROADCAST_CONNECTION" value="null"/>
<env name="CACHE_STORE" value="array"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="DB_URL" value=""/>
<env name="MAIL_MAILER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="PULSE_ENABLED" value="false"/>
<env name="TELESCOPE_ENABLED" value="false"/>
<env name="NIGHTWATCH_ENABLED" value="false"/>
</php>
</phpunit>
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
+25
View File
@@ -0,0 +1,25 @@
<IfModule mod_rewrite.c>
<IfModule mod_negotiation.c>
Options -MultiViews -Indexes
</IfModule>
RewriteEngine On
# Handle Authorization Header
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
# Handle X-XSRF-Token Header
RewriteCond %{HTTP:x-xsrf-token} .
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
# Redirect Trailing Slashes If Not A Folder...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [L,R=301]
# Send Requests To Front Controller...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</IfModule>
View File
File diff suppressed because one or more lines are too long
+17
View File
@@ -0,0 +1,17 @@
{
"resources/css/app.css": {
"file": "assets/app-CyRyejKf.css",
"name": "app",
"names": [
"app.css"
],
"src": "resources/css/app.css",
"isEntry": true
},
"resources/js/app.js": {
"file": "assets/app-BvRk9kiK.js",
"name": "app",
"src": "resources/js/app.js",
"isEntry": true
}
}
View File
+20
View File
@@ -0,0 +1,20 @@
<?php
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
define('LARAVEL_START', microtime(true));
// Determine if the application is in maintenance mode...
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
require $maintenance;
}
// Register the Composer autoloader...
require __DIR__.'/../vendor/autoload.php';
// Bootstrap Laravel and handle the request...
/** @var Application $app */
$app = require_once __DIR__.'/../bootstrap/app.php';
$app->handleRequest(Request::capture());
+2
View File
@@ -0,0 +1,2 @@
User-agent: *
Disallow:
+3
View File
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

Some files were not shown because too many files have changed in this diff Show More