MIF_E31230892/sim-pkpps/app/Console/Commands/MigrateSantriToNewKelas.php

310 lines
11 KiB
PHP

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use App\Models\Santri;
use App\Models\Kelas;
use App\Models\KelompokKelas;
use App\Models\SantriKelas;
class MigrateSantriToNewKelas extends Command
{
/**
* The name and signature of the console command.
*/
protected $signature = 'migrate:santri-kelas-full
{--dry-run : Preview tanpa menyimpan ke database}';
/**
* The console command description.
*/
protected $description = 'Full migration: Pindahkan data kolom kelas santri ke tabel santri_kelas (sistem baru)';
/**
* Counters
*/
protected int $totalSantri = 0;
protected int $successCount = 0;
protected int $skipCount = 0;
protected int $errorCount = 0;
/**
* Collected errors & skipped
*/
protected array $errors = [];
protected array $skipped = [];
/**
* Resolved kelas mapping cache: ['PB' => Kelas model, ...]
*/
protected array $kelasMapping = [];
/**
* Execute the console command.
*/
public function handle(): int
{
$isDryRun = $this->option('dry-run');
$this->newLine();
$this->info('╔══════════════════════════════════════════════════════╗');
$this->info('║ MIGRASI SANTRI KE SISTEM KELAS BARU ║');
$this->info('║ ' . ($isDryRun ? '🔍 MODE: DRY-RUN (Preview Only)' : '🚀 MODE: EXECUTE (Real Migration)') . ' ║');
$this->info('╚══════════════════════════════════════════════════════╝');
$this->newLine();
// ────────────────────
// STEP 1: Validasi kelompok kelas
// ────────────────────
$this->info('📋 Step 1: Validasi kelompok kelas...');
if (!$this->validateAndBuildMapping()) {
$this->error('❌ Validasi gagal! Pastikan data kelompok_kelas dan kelas sudah tersedia.');
return Command::FAILURE;
}
$this->info(' ✅ Mapping kelas berhasil di-resolve:');
foreach ($this->kelasMapping as $oldKelas => $kelasModel) {
$this->line(" <fg=cyan>{$oldKelas}</> → <fg=green>{$kelasModel->kode_kelas} ({$kelasModel->nama_kelas})</>");
}
$this->newLine();
// ────────────────────
// STEP 2: Ambil semua santri
// ────────────────────
$this->info('📋 Step 2: Mengambil data santri...');
$santris = Santri::select('id', 'id_santri', 'nama_lengkap', 'kelas')->get();
$this->totalSantri = $santris->count();
if ($this->totalSantri === 0) {
$this->warn('⚠️ Tidak ada data santri ditemukan.');
return Command::SUCCESS;
}
$this->info(" 📊 Total santri ditemukan: <fg=yellow>{$this->totalSantri}</>");
$this->newLine();
// ────────────────────
// STEP 3: Migrate
// ────────────────────
$tahunAjaran = SantriKelas::getCurrentAcademicYear();
$this->info("📋 Step 3: Memulai migrasi (Tahun Ajaran: <fg=yellow>{$tahunAjaran}</>)...");
$this->newLine();
if (!$isDryRun) {
// Wrap dalam transaction untuk safety
DB::beginTransaction();
}
try {
$this->output->progressStart($this->totalSantri);
foreach ($santris as $santri) {
$this->processSantri($santri, $tahunAjaran, $isDryRun);
$this->output->progressAdvance();
}
$this->output->progressFinish();
$this->newLine();
if (!$isDryRun) {
DB::commit();
$this->info('✅ Transaction committed.');
}
} catch (\Exception $e) {
if (!$isDryRun) {
DB::rollBack();
$this->error('❌ Transaction rolled back!');
}
$this->error("Fatal error: {$e->getMessage()}");
Log::error('MigrateSantriToNewKelas fatal error', [
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return Command::FAILURE;
}
// ────────────────────
// STEP 4: Summary Report
// ────────────────────
$this->printSummary($isDryRun, $tahunAjaran);
// ────────────────────
// STEP 5: Post-migration validation
// ────────────────────
if (!$isDryRun) {
$this->validatePostMigration();
}
return Command::SUCCESS;
}
/**
* Validasi kelompok kelas dan build mapping dinamis.
*/
protected function validateAndBuildMapping(): bool
{
$mappings = [
'PB' => '%PB%',
'Lambatan' => '%Lambatan%',
'Cepatan' => '%Cepatan%',
];
foreach ($mappings as $oldKelas => $likePattern) {
$kelas = Kelas::whereHas('kelompok', function ($q) use ($likePattern) {
$q->where('nama_kelompok', 'like', $likePattern);
})
->where('is_active', true)
->orderBy('urutan')
->first();
if (!$kelas) {
$this->error(" ❌ Tidak ditemukan kelas aktif untuk kelompok '{$oldKelas}' (pattern: {$likePattern})");
return false;
}
$this->kelasMapping[$oldKelas] = $kelas;
}
return true;
}
/**
* Process satu santri.
*/
protected function processSantri(Santri $santri, string $tahunAjaran, bool $isDryRun): void
{
try {
$kelasLama = $santri->kelas;
// Skip jika kelas NULL atau tidak dikenali
if (empty($kelasLama) || !isset($this->kelasMapping[$kelasLama])) {
$reason = empty($kelasLama) ? 'Kelas NULL' : "Kelas '{$kelasLama}' tidak dikenali";
$this->skipped[] = [
'id_santri' => $santri->id_santri,
'nama' => $santri->nama_lengkap,
'reason' => $reason,
];
$this->skipCount++;
return;
}
$kelasBaru = $this->kelasMapping[$kelasLama];
if ($isDryRun) {
// Dry-run: hanya tampilkan
$this->line(" <fg=green>✓</> {$santri->id_santri} ({$santri->nama_lengkap}): <fg=yellow>{$kelasLama}</> → <fg=cyan>{$kelasBaru->kode_kelas} ({$kelasBaru->nama_kelas})</>");
$this->successCount++;
return;
}
// Real execute: Insert/update ke santri_kelas
SantriKelas::updateOrCreate(
[
'id_santri' => $santri->id_santri,
'tahun_ajaran' => $tahunAjaran,
'is_primary' => true,
],
[
'id_kelas' => $kelasBaru->id,
]
);
$this->successCount++;
} catch (\Exception $e) {
$this->errors[] = [
'id_santri' => $santri->id_santri,
'nama' => $santri->nama_lengkap,
'error' => $e->getMessage(),
];
$this->errorCount++;
Log::warning('MigrateSantriToNewKelas: Error processing santri', [
'id_santri' => $santri->id_santri,
'error' => $e->getMessage(),
]);
}
}
/**
* Print summary report.
*/
protected function printSummary(bool $isDryRun, string $tahunAjaran): void
{
$this->newLine();
$this->info('╔══════════════════════════════════════════════════════╗');
$this->info('║ 📊 SUMMARY REPORT ║');
$this->info('╚══════════════════════════════════════════════════════╝');
$this->newLine();
$this->line(" Mode : <fg=" . ($isDryRun ? 'yellow>DRY-RUN (Preview)' : 'green>EXECUTED (Real)') . "</>");
$this->line(" Tahun Ajaran : <fg=cyan>{$tahunAjaran}</>");
$this->newLine();
$this->line(" Total santri : <fg=white>{$this->totalSantri}</>");
$this->line(" ✅ Berhasil : <fg=green>{$this->successCount}</>");
$this->line(" ⚠️ Skipped : <fg=yellow>{$this->skipCount}</>");
$this->line(" ❌ Error : <fg=red>{$this->errorCount}</>");
// List skipped
if (count($this->skipped) > 0) {
$this->newLine();
$this->warn(' ⚠️ Santri yang di-skip:');
foreach ($this->skipped as $item) {
$this->line(" - <fg=yellow>{$item['id_santri']}</> ({$item['nama']}): {$item['reason']}");
}
}
// List errors
if (count($this->errors) > 0) {
$this->newLine();
$this->error(' ❌ Santri yang error:');
foreach ($this->errors as $item) {
$this->line(" - <fg=red>{$item['id_santri']}</> ({$item['nama']}): {$item['error']}");
}
}
$this->newLine();
if ($isDryRun) {
$this->info('💡 Ini hanya preview. Jalankan tanpa --dry-run untuk eksekusi migrasi.');
} else {
$this->info('✅ Migrasi selesai! Data santri_kelas telah diperbarui.');
}
$this->newLine();
}
/**
* Validasi setelah migrasi.
*/
protected function validatePostMigration(): void
{
$this->info('📋 Post-migration validation...');
// Count santri yang punya kelas (kolom lama) tapi belum ada di santri_kelas
$santriDenganKelas = Santri::whereNotNull('kelas')
->where('kelas', '!=', '')
->count();
$santriDiSantriKelas = SantriKelas::where('is_primary', true)->count();
$this->line(" Santri dengan kelas (kolom lama) : <fg=yellow>{$santriDenganKelas}</>");
$this->line(" Santri di santri_kelas (primary) : <fg=cyan>{$santriDiSantriKelas}</>");
if ($santriDiSantriKelas >= $santriDenganKelas) {
$this->info(' ✅ Validasi OK! Semua santri sudah ter-migrate.');
} else {
$diff = $santriDenganKelas - $santriDiSantriKelas;
$this->warn(" ⚠️ Ada {$diff} santri yang belum ter-migrate. Periksa log di atas.");
}
$this->newLine();
}
}