perbaikan
This commit is contained in:
parent
51fc725c41
commit
ce3ee0d1a3
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Python / FastAPI Core
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Python Environments
|
||||||
|
.venv/
|
||||||
|
.env/
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
|
||||||
|
# Python Testing, Logs, & Coverage
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
*.log
|
||||||
|
fastapi/ml_assets/tmp/
|
||||||
|
|
||||||
|
# Environments & Secrets (Global & FastAPI)
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# OS & Editor / IDE (Untuk level Root)
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
*.swp
|
||||||
|
*.bak
|
||||||
|
|
||||||
|
# Model / Data (Jika di-generate/download lokal)
|
||||||
|
fastapi/ml_assets/*.pkl
|
||||||
|
fastapi/ml_assets/*.h5
|
||||||
|
fastapi/ml_assets/*.pt
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
FASTAPI_SECRET_KEY=
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from fastapi import FastAPI, UploadFile, File, HTTPException, BackgroundTasks
|
from fastapi import FastAPI, UploadFile, File, HTTPException, BackgroundTasks, Depends, Security
|
||||||
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
@ -10,6 +11,7 @@ import uuid
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
import logging
|
import logging
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -69,6 +71,18 @@ async def lifespan(app: FastAPI):
|
||||||
|
|
||||||
app = FastAPI(title="Tracer Study Classification Worker", lifespan=lifespan)
|
app = FastAPI(title="Tracer Study Classification Worker", lifespan=lifespan)
|
||||||
|
|
||||||
|
# --- SECURITY SETUP ---
|
||||||
|
security = HTTPBearer()
|
||||||
|
EXPECTED_TOKEN = os.getenv("FASTAPI_SECRET_KEY", "")
|
||||||
|
|
||||||
|
def verify_token(credentials: HTTPAuthorizationCredentials = Security(security)):
|
||||||
|
if not EXPECTED_TOKEN:
|
||||||
|
return credentials.credentials
|
||||||
|
if credentials.credentials != EXPECTED_TOKEN:
|
||||||
|
raise HTTPException(status_code=401, detail="Unauthorized - Invalid Token")
|
||||||
|
return credentials.credentials
|
||||||
|
# ----------------------
|
||||||
|
|
||||||
def clean_text(text: str) -> str:
|
def clean_text(text: str) -> str:
|
||||||
if pd.isna(text) or not isinstance(text, str):
|
if pd.isna(text) or not isinstance(text, str):
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -102,7 +116,7 @@ def safe_get(row: pd.Series, df_columns: list, code: str, default: str = "") ->
|
||||||
def classify_rule(job_text: str) -> dict:
|
def classify_rule(job_text: str) -> dict:
|
||||||
clean = clean_text(job_text)
|
clean = clean_text(job_text)
|
||||||
if not clean or len(clean) < 3:
|
if not clean or len(clean) < 3:
|
||||||
return {"profile": "Non-IT", "confidence": 0.65, "method": "rule_based_fallback"}
|
return {"profile": "Tidak Diketahui", "confidence": 0.65, "method": "rule_based_fallback"}
|
||||||
|
|
||||||
for profile, keywords in KEYWORD_RULES.items():
|
for profile, keywords in KEYWORD_RULES.items():
|
||||||
if any(kw in clean for kw in keywords):
|
if any(kw in clean for kw in keywords):
|
||||||
|
|
@ -112,7 +126,7 @@ def classify_rule(job_text: str) -> dict:
|
||||||
def classify_ml(job_text: str, pipeline) -> dict:
|
def classify_ml(job_text: str, pipeline) -> dict:
|
||||||
clean = clean_text(job_text)
|
clean = clean_text(job_text)
|
||||||
if not clean:
|
if not clean:
|
||||||
return {"profile": "Non-IT", "confidence": 0.0, "method": "ml_empty_input"}
|
return {"profile": "Tidak Diketahui", "confidence": 0.0, "method": "ml_empty_input"}
|
||||||
try:
|
try:
|
||||||
proba = pipeline.predict_proba([clean])[0]
|
proba = pipeline.predict_proba([clean])[0]
|
||||||
max_conf = float(np.max(proba))
|
max_conf = float(np.max(proba))
|
||||||
|
|
@ -163,7 +177,7 @@ def health_check():
|
||||||
|
|
||||||
|
|
||||||
# RE-TRAINING ENDPOINTS
|
# RE-TRAINING ENDPOINTS
|
||||||
@app.post("/api/v1/retrain")
|
@app.post("/api/v1/retrain", dependencies=[Depends(verify_token)])
|
||||||
async def trigger_retrain(file: UploadFile = File(None)):
|
async def trigger_retrain(file: UploadFile = File(None)):
|
||||||
if STATUS_PATH.exists():
|
if STATUS_PATH.exists():
|
||||||
try:
|
try:
|
||||||
|
|
@ -214,7 +228,7 @@ async def trigger_retrain(file: UploadFile = File(None)):
|
||||||
return JSONResponse(content={"status": "started", "message": "Re-training dimulai di background."})
|
return JSONResponse(content={"status": "started", "message": "Re-training dimulai di background."})
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/v1/retrain/status")
|
@app.get("/api/v1/retrain/status", dependencies=[Depends(verify_token)])
|
||||||
def retrain_status():
|
def retrain_status():
|
||||||
if not STATUS_PATH.exists():
|
if not STATUS_PATH.exists():
|
||||||
return JSONResponse(content={"stage": "idle", "message": "Belum ada proses re-training."})
|
return JSONResponse(content={"stage": "idle", "message": "Belum ada proses re-training."})
|
||||||
|
|
@ -237,7 +251,7 @@ def retrain_status():
|
||||||
return JSONResponse(content=data)
|
return JSONResponse(content=data)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/v1/retrain/reload")
|
@app.post("/api/v1/retrain/reload", dependencies=[Depends(verify_token)])
|
||||||
def reload_model():
|
def reload_model():
|
||||||
try:
|
try:
|
||||||
app.state.pipeline = joblib.load(PIPELINE_PATH)
|
app.state.pipeline = joblib.load(PIPELINE_PATH)
|
||||||
|
|
@ -246,7 +260,7 @@ def reload_model():
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"Gagal reload model: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Gagal reload model: {str(e)}")
|
||||||
|
|
||||||
@app.post("/api/v1/classify")
|
@app.post("/api/v1/classify", dependencies=[Depends(verify_token)])
|
||||||
async def classify_tracer(file: UploadFile = File(...)):
|
async def classify_tracer(file: UploadFile = File(...)):
|
||||||
if not file.filename:
|
if not file.filename:
|
||||||
raise HTTPException(status_code=400, detail="No file provided")
|
raise HTTPException(status_code=400, detail="No file provided")
|
||||||
|
|
@ -348,7 +362,7 @@ async def classify_tracer(file: UploadFile = File(...)):
|
||||||
|
|
||||||
rule_res = classify_rule(job_text)
|
rule_res = classify_rule(job_text)
|
||||||
res = rule_res or (classify_ml(job_text, pipeline) if pipeline else {"profile": "Non-IT", "confidence": 0.0, "method": "ml_unavailable"})
|
res = rule_res or (classify_ml(job_text, pipeline) if pipeline else {"profile": "Non-IT", "confidence": 0.0, "method": "ml_unavailable"})
|
||||||
status = "auto_classified" if res["method"] in ["rule_based", "ml_fallback"] else "needs_review"
|
status = "auto_classified" if res["method"] in ["rule_based", "ml_fallback"] or res["profile"] == "Tidak Diketahui" else "needs_review"
|
||||||
|
|
||||||
raw_data_payload = {str(k): safe_str(v) for k, v in row.to_dict().items()}
|
raw_data_payload = {str(k): safe_str(v) for k, v in row.to_dict().items()}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"stage": "rolled_back",
|
||||||
|
"message": "Model lama dipertahankan. Model baru lebih buruk secara signifikan (-12.92% weighted F1)",
|
||||||
|
"timestamp": "2026-05-25T06:51:07.653672",
|
||||||
|
"result": "rolled_back",
|
||||||
|
"reason": "Model baru lebih buruk secara signifikan (-12.92% weighted F1)",
|
||||||
|
"old_f1": 0.7804040674617057,
|
||||||
|
"new_f1": 0.6512,
|
||||||
|
"_reloaded": true
|
||||||
|
}
|
||||||
|
|
@ -1,371 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use App\Models\ClassificationResult;
|
|
||||||
use App\Models\KemendikRawData;
|
|
||||||
use App\Services\FastApiWorkerService;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
class ClassificationController extends Controller
|
|
||||||
{
|
|
||||||
public function __construct(protected FastApiWorkerService $fastApi) {}
|
|
||||||
|
|
||||||
public function upload(Request $request) {
|
|
||||||
$request->validate(['file' => 'required|file|mimes:csv,xlsx,xls|max:20480']);
|
|
||||||
|
|
||||||
$result = $this->fastApi->classifyFile($request->file('file')->getRealPath(), $request->file('file')->getClientOriginalName());
|
|
||||||
if (isset($result['error'])) return back()->with('error', $result['error']);
|
|
||||||
|
|
||||||
DB::transaction(function () use ($result) {
|
|
||||||
foreach ($result['results'] as $row) {
|
|
||||||
if ($row['source_type'] === 'kemendik') {
|
|
||||||
$raw = $row['raw_data'] ?? [];
|
|
||||||
KemendikRawData::updateOrCreate(
|
|
||||||
['nimhsmsmh' => $row['nim'], 'source_type' => 'kemendik'],
|
|
||||||
[
|
|
||||||
'nmmhsmsmh' => $row['nama'], 'tahun_lulus' => $row['tahun_lulus'] ?? null,
|
|
||||||
'f5c_jabatan_kode' => $raw['F5c'] ?? null, 'f5c_jabatan_text' => $this->mapF5c($raw['F5c'] ?? null),
|
|
||||||
'f1101_jenis_instansi_kode' => $raw['F1101'] ?? null, 'f1101_jenis_instansi_text' => $this->mapF1101($raw['F1101'] ?? null),
|
|
||||||
'f5a1_provinsi' => $raw['F5a1'] ?? null, 'raw_data' => $raw
|
|
||||||
]
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ClassificationResult::updateOrCreate(
|
|
||||||
['nim' => $row['nim'], 'source_type' => 'internal_mif'],
|
|
||||||
[
|
|
||||||
'nama' => $row['nama'], 'tahun_lulus' => $row['tahun_lulus'] ?? null,
|
|
||||||
'job_text_raw' => $row['job_text_raw'], 'predicted_profile' => $row['predicted_profile'],
|
|
||||||
'confidence_score' => $row['confidence_score'], 'classification_method' => $row['classification_method'],
|
|
||||||
'status' => $row['status'], 'error_detail' => $row['error_detail'] ?? null
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isset($row['raw_data'])) {
|
|
||||||
\App\Models\InternalRawData::updateOrCreate(
|
|
||||||
['nim' => $row['nim']],
|
|
||||||
['raw_data' => $row['raw_data']]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (empty($result['results'])) {
|
|
||||||
return back()->with('success', "Selesai. 0 data berhasil diproses.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$isKemendik = $result['results'][0]['source_type'] === 'kemendik';
|
|
||||||
if ($isKemendik) {
|
|
||||||
$count = collect($result['results'])->where('source_type','kemendik')->count();
|
|
||||||
return back()->with('success', "Selesai. {$result['processed_rows']} baris diproses. {$count} data Kemendik berhasil masuk.");
|
|
||||||
} else {
|
|
||||||
$classified = collect($result['results'])->where('source_type','internal_mif')->where('status','auto_classified')->count();
|
|
||||||
return back()->with('success', "Selesai. {$result['processed_rows']} data diproses. {$classified} diklasifikasi otomatis (Internal MIF).");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function mapF5c(?string $c): ?string { return ['1'=>'Founder','2'=>'Co-Founder','3'=>'Staff','4'=>'Freelance'][$c] ?? null; }
|
|
||||||
private function mapF1101(?string $c): ?string { return ['1'=>'Pemerintah','2'=>'Non-profit','3'=>'Swasta','4'=>'Wiraswasta','6'=>'BUMN','7'=>'Multilateral'][$c] ?? null; }
|
|
||||||
|
|
||||||
public function dashboard(Request $request)
|
|
||||||
{
|
|
||||||
$stats = [
|
|
||||||
'total_internal' => \App\Models\ClassificationResult::where('source_type', 'internal_mif')->count(),
|
|
||||||
'auto_classified' => \App\Models\ClassificationResult::where('source_type', 'internal_mif')->where('status', 'auto_classified')->count(),
|
|
||||||
'needs_review' => \App\Models\ClassificationResult::where('source_type', 'internal_mif')->where('status', 'needs_review')->count(),
|
|
||||||
];
|
|
||||||
|
|
||||||
$query = \App\Models\ClassificationResult::where('source_type', 'internal_mif');
|
|
||||||
|
|
||||||
// Always put needs_review first unless overridden by a very specific custom sort requirement (we'll just prepend it to orders)
|
|
||||||
$query->orderByRaw("FIELD(status, 'needs_review', 'manual_override', 'auto_classified', 'failed')");
|
|
||||||
|
|
||||||
if ($request->has('sort')) {
|
|
||||||
$allowedSorts = ['nim', 'nama', 'job_text_raw', 'predicted_profile', 'status'];
|
|
||||||
if (in_array($request->sort, $allowedSorts)) {
|
|
||||||
$query->orderBy($request->sort, $request->direction === 'desc' ? 'desc' : 'asc');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$query->latest();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Charts Data Aggregation
|
|
||||||
$allResults = \App\Models\ClassificationResult::where('source_type', 'internal_mif')->get();
|
|
||||||
$allRaws = \App\Models\InternalRawData::all();
|
|
||||||
|
|
||||||
$chartProfile = $allResults->whereNotNull('predicted_profile')->groupBy('predicted_profile')->map->count();
|
|
||||||
$chartMethod = $allResults->whereNotNull('classification_method')->groupBy('classification_method')->map->count();
|
|
||||||
|
|
||||||
$waktuTunggu = [];
|
|
||||||
foreach ($allRaws as $r) {
|
|
||||||
$tahun = $r->raw_data['tahun_lulus'] ?? null;
|
|
||||||
$tunggu = intval($r->raw_data['total_masa_tunggu'] ?? 0);
|
|
||||||
if (!empty($tahun) && $tunggu > 0) {
|
|
||||||
$waktuTunggu[$tahun][] = $tunggu;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$chartWaktuTunggu = collect($waktuTunggu)->map(fn($arr) => round(collect($arr)->average(), 1))->sortKeys();
|
|
||||||
|
|
||||||
$chartFunnel = [
|
|
||||||
'Lamaran Dikirim' => round((float)$allRaws->map(fn($r) => intval($r->raw_data['jumlah_lamaran_dikirim'] ?? 0))->average(), 1),
|
|
||||||
'Respons' => round((float)$allRaws->map(fn($r) => intval($r->raw_data['jumlah_respons_lamaran'] ?? 0))->average(), 1),
|
|
||||||
'Wawancara' => round((float)$allRaws->map(fn($r) => intval($r->raw_data['jumlah_undangan_wawancara'] ?? 0))->average(), 1),
|
|
||||||
];
|
|
||||||
|
|
||||||
$chartLokasi = $allRaws->map(fn($r) => $r->raw_data['lokasi_perusahaan'] ?? null)
|
|
||||||
->map(fn($i) => trim($i))
|
|
||||||
->filter(fn($i) => !empty($i))
|
|
||||||
->groupBy(fn($i) => $i)->map->count();
|
|
||||||
|
|
||||||
$chartTopLokasi = $chartLokasi->sortDesc()->take(5);
|
|
||||||
|
|
||||||
$charts = [
|
|
||||||
'profile' => $chartProfile,
|
|
||||||
'method' => $chartMethod,
|
|
||||||
'waktu_tunggu' => $chartWaktuTunggu,
|
|
||||||
'funnel' => $chartFunnel,
|
|
||||||
'lokasi' => $chartTopLokasi,
|
|
||||||
'map_lokasi' => $chartLokasi,
|
|
||||||
];
|
|
||||||
|
|
||||||
// Load ML metrics untuk panel re-training
|
|
||||||
$metricsPath = base_path('../fastapi/ml_assets/metrics_internal_only.json');
|
|
||||||
$mlMetrics = null;
|
|
||||||
if (file_exists($metricsPath)) {
|
|
||||||
$mlMetrics = json_decode(file_get_contents($metricsPath), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
$manualOverrideCount = \App\Models\ClassificationResult::where('source_type', 'internal_mif')
|
|
||||||
->where('status', 'manual_override')
|
|
||||||
->whereNotNull('job_text_raw')
|
|
||||||
->count();
|
|
||||||
|
|
||||||
return view('dashboard', [
|
|
||||||
'stats' => $stats,
|
|
||||||
'charts' => $charts,
|
|
||||||
'internal_data' => $query->paginate(10)->appends($request->query()),
|
|
||||||
'ml_metrics' => $mlMetrics,
|
|
||||||
'manual_override_count' => $manualOverrideCount,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public function update(Request $request, $id)
|
|
||||||
{
|
|
||||||
$request->validate([
|
|
||||||
'predicted_profile' => 'required|in:Programmer,Data Analyst,Wirausaha Informatika,Non-IT',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$record = \App\Models\ClassificationResult::findOrFail($id);
|
|
||||||
$record->update([
|
|
||||||
'predicted_profile' => $request->predicted_profile,
|
|
||||||
'status' => 'manual_override'
|
|
||||||
]);
|
|
||||||
|
|
||||||
return back()->with('success', 'Data berhasil diperbarui!');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function destroy($id)
|
|
||||||
{
|
|
||||||
$record = \App\Models\ClassificationResult::findOrFail($id);
|
|
||||||
$record->delete();
|
|
||||||
|
|
||||||
return back()->with('success', 'Data berhasil dihapus!');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function bulkDestroy(Request $request)
|
|
||||||
{
|
|
||||||
$request->validate([
|
|
||||||
'ids' => 'required|array',
|
|
||||||
'ids.*' => 'exists:classification_results,id',
|
|
||||||
]);
|
|
||||||
|
|
||||||
\App\Models\ClassificationResult::whereIn('id', $request->ids)->delete();
|
|
||||||
|
|
||||||
return back()->with('success', count($request->ids) . ' data berhasil dihapus!');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function bulkUpdate(Request $request)
|
|
||||||
{
|
|
||||||
$request->validate([
|
|
||||||
'ids' => 'required|array',
|
|
||||||
'ids.*' => 'exists:classification_results,id',
|
|
||||||
'predicted_profile' => 'required|in:Programmer,Data Analyst,Wirausaha Informatika,Non-IT',
|
|
||||||
]);
|
|
||||||
|
|
||||||
\App\Models\ClassificationResult::whereIn('id', $request->ids)->update([
|
|
||||||
'predicted_profile' => $request->predicted_profile,
|
|
||||||
'status' => 'manual_override'
|
|
||||||
]);
|
|
||||||
|
|
||||||
return back()->with('success', count($request->ids) . ' data berhasil diubah profilnya!');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function kemendik()
|
|
||||||
{
|
|
||||||
$stats = [
|
|
||||||
'total_kemendik' => \App\Models\KemendikRawData::count(),
|
|
||||||
];
|
|
||||||
|
|
||||||
$keteranganPath = base_path('../data/keterangan_ts_kemendiktisaintek.csv');
|
|
||||||
$keterangan = [];
|
|
||||||
if (file_exists($keteranganPath)) {
|
|
||||||
$lines = file($keteranganPath);
|
|
||||||
if (count($lines) > 0) {
|
|
||||||
$rawHeaders = str_getcsv(trim($lines[0]), ';');
|
|
||||||
$headers = array_map('trim', $rawHeaders);
|
|
||||||
|
|
||||||
for ($i = 1; $i < count($lines); $i++) {
|
|
||||||
$row = str_getcsv(trim($lines[$i]), ';');
|
|
||||||
foreach ($headers as $index => $header) {
|
|
||||||
$val = trim($row[$index] ?? '');
|
|
||||||
if ($val !== '') {
|
|
||||||
if (preg_match('/^(\d+)\s*-\s*(.*)/', $val, $matches)) {
|
|
||||||
$keterangan[$header][$matches[1]] = $val;
|
|
||||||
} else {
|
|
||||||
$keterangan[$header][$val] = $val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$allRaws = \App\Models\KemendikRawData::all();
|
|
||||||
|
|
||||||
// Helper to count occurrences
|
|
||||||
$countBy = function($key) use ($allRaws, $keterangan) {
|
|
||||||
return $allRaws->map(function($r) use ($key, $keterangan) {
|
|
||||||
$val = trim($r->raw_data[$key] ?? '');
|
|
||||||
return $val !== '' ? ($keterangan[$key][$val] ?? $val) : null;
|
|
||||||
})->filter()->groupBy(fn($i) => $i)->map->count();
|
|
||||||
};
|
|
||||||
|
|
||||||
$chartStatus = $countBy('F8 (Jelaskan status Anda saat ini?)');
|
|
||||||
$chartInstansi = $countBy('F1101 (Apa jenis perusahaan/instansi/institusi tempat anda bekerja sekarang?)');
|
|
||||||
$chartKesesuaian = $countBy('F14 (Seberapa erat hubungan bidang studi dengan pekerjaan Anda?)');
|
|
||||||
$chartLevel = $countBy('F5d (Apa tingkatan tempat kerja Anda?)');
|
|
||||||
|
|
||||||
// Pendapatan F505
|
|
||||||
$chartPendapatanRaw = $allRaws->map(fn($r) => floatval($r->raw_data['F505 (Berapa rata-rata pendapatan Anda per bulan?)'] ?? 0))->filter(fn($v) => $v > 0);
|
|
||||||
$chartPendapatan = [
|
|
||||||
'< 2 Juta' => $chartPendapatanRaw->filter(fn($v) => $v < 2000000)->count(),
|
|
||||||
'2 - 4 Juta' => $chartPendapatanRaw->filter(fn($v) => $v >= 2000000 && $v <= 4000000)->count(),
|
|
||||||
'4 - 6 Juta' => $chartPendapatanRaw->filter(fn($v) => $v > 4000000 && $v <= 6000000)->count(),
|
|
||||||
'> 6 Juta' => $chartPendapatanRaw->filter(fn($v) => $v > 6000000)->count(),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Map Lokasi
|
|
||||||
$mapLokasi = $allRaws->map(fn($r) => $r->raw_data['F5a2 (Dimana lokasi kabupaten/kota tempat Anda bekerja?)'] ?? null)->filter()->groupBy(fn($i) => $i)->map->count();
|
|
||||||
|
|
||||||
$coordsPath = base_path('../data/kabupaten_coords.json');
|
|
||||||
$kabupatenCoords = file_exists($coordsPath) ? json_decode(file_get_contents($coordsPath), true) : [];
|
|
||||||
|
|
||||||
$charts = [
|
|
||||||
'status' => $chartStatus,
|
|
||||||
'instansi' => $chartInstansi,
|
|
||||||
'kesesuaian' => $chartKesesuaian,
|
|
||||||
'pendapatan' => array_filter($chartPendapatan), // remove zero values
|
|
||||||
'level' => $chartLevel,
|
|
||||||
'map_lokasi' => $mapLokasi,
|
|
||||||
'coords' => $kabupatenCoords
|
|
||||||
];
|
|
||||||
|
|
||||||
return view('kemendik', [
|
|
||||||
'stats' => $stats,
|
|
||||||
'charts' => $charts,
|
|
||||||
'kemendik_data' => \App\Models\KemendikRawData::orderBy('imported_at', 'desc')->paginate(10),
|
|
||||||
'keterangan' => $keterangan
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function downloadPdf(Request $request)
|
|
||||||
{
|
|
||||||
$query = \App\Models\ClassificationResult::where('source_type', 'internal_mif');
|
|
||||||
|
|
||||||
$query->orderByRaw("FIELD(status, 'needs_review', 'manual_override', 'auto_classified', 'failed')");
|
|
||||||
|
|
||||||
if ($request->has('sort')) {
|
|
||||||
$allowedSorts = ['nim', 'nama', 'job_text_raw', 'predicted_profile', 'status'];
|
|
||||||
if (in_array($request->sort, $allowedSorts)) {
|
|
||||||
$query->orderBy($request->sort, $request->direction === 'desc' ? 'desc' : 'asc');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$query->latest();
|
|
||||||
}
|
|
||||||
|
|
||||||
$internal_data = $query->get();
|
|
||||||
|
|
||||||
$pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('pdf.classification_results', compact('internal_data'));
|
|
||||||
|
|
||||||
return $pdf->download('Hasil_Klasifikasi_Internal_MIF.pdf');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ekspor data manual_override ke CSV sementara, lalu trigger re-training via FastAPI.
|
|
||||||
* Returns JSON untuk AJAX.
|
|
||||||
*/
|
|
||||||
public function retrain(Request $request)
|
|
||||||
{
|
|
||||||
// Ambil semua data manual_override yang memiliki job_text_raw
|
|
||||||
$overrideData = ClassificationResult::where('source_type', 'internal_mif')
|
|
||||||
->where('status', 'manual_override')
|
|
||||||
->whereNotNull('job_text_raw')
|
|
||||||
->whereNotNull('predicted_profile')
|
|
||||||
->get(['job_text_raw', 'predicted_profile']);
|
|
||||||
|
|
||||||
$csvPath = null;
|
|
||||||
|
|
||||||
if ($overrideData->count() > 0) {
|
|
||||||
// Buat CSV sementara di storage/app/temp/
|
|
||||||
$csvLines = ['job_text_raw;label'];
|
|
||||||
foreach ($overrideData as $row) {
|
|
||||||
$text = str_replace([';', "\n", "\r"], [',', ' ', ' '], $row->job_text_raw);
|
|
||||||
$label = $row->predicted_profile;
|
|
||||||
$csvLines[] = "{$text};{$label}";
|
|
||||||
}
|
|
||||||
|
|
||||||
$tmpDir = storage_path('app/temp');
|
|
||||||
if (!is_dir($tmpDir)) {
|
|
||||||
mkdir($tmpDir, 0755, true);
|
|
||||||
}
|
|
||||||
$csvPath = $tmpDir . '/manual_override_' . time() . '.csv';
|
|
||||||
file_put_contents($csvPath, implode("\n", $csvLines));
|
|
||||||
|
|
||||||
Log::info("Retrain: Exported {$overrideData->count()} manual_override rows to {$csvPath}");
|
|
||||||
} else {
|
|
||||||
Log::info('Retrain: Tidak ada data manual_override, training hanya dengan corpus asli.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger FastAPI
|
|
||||||
$result = $this->fastApi->triggerRetrain($csvPath);
|
|
||||||
|
|
||||||
// Bersihkan CSV temp
|
|
||||||
if ($csvPath && file_exists($csvPath)) {
|
|
||||||
@unlink($csvPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($result['error'])) {
|
|
||||||
return response()->json(['success' => false, 'message' => $result['error']], 422);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'success' => true,
|
|
||||||
'message' => 'Re-training dimulai di background.',
|
|
||||||
'override_count' => $overrideData->count(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Proxy polling status re-training dari FastAPI ke frontend.
|
|
||||||
*/
|
|
||||||
public function retrainStatus(Request $request)
|
|
||||||
{
|
|
||||||
$status = $this->fastApi->getRetrainStatus();
|
|
||||||
return response()->json($status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,195 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\ClassificationResult;
|
||||||
|
use App\Models\KemendikRawData;
|
||||||
|
use App\Models\InternalRawData;
|
||||||
|
use App\Services\FastApiWorkerService;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class ClassificationTaskController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(protected FastApiWorkerService $fastApi) {}
|
||||||
|
|
||||||
|
public function upload(Request $request) {
|
||||||
|
$request->validate(['file' => 'required|file|mimes:csv,xlsx,xls|max:20480']);
|
||||||
|
|
||||||
|
$result = $this->fastApi->classifyFile($request->file('file')->getRealPath(), $request->file('file')->getClientOriginalName());
|
||||||
|
|
||||||
|
if (!isset($result['results']) && isset($result['error'])) {
|
||||||
|
return back()->with('error', $result['error']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($result['results'])) {
|
||||||
|
return back()->with('success', "Selesai. 0 data berhasil diproses.");
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::transaction(function () use ($result) {
|
||||||
|
$kemendikUpsertData = [];
|
||||||
|
$internalResultsUpsertData = [];
|
||||||
|
$internalRawUpsertData = [];
|
||||||
|
|
||||||
|
foreach ($result['results'] as $row) {
|
||||||
|
if ($row['source_type'] === 'kemendik') {
|
||||||
|
$raw = $row['raw_data'] ?? [];
|
||||||
|
$kemendikUpsertData[] = [
|
||||||
|
'nimhsmsmh' => $row['nim'],
|
||||||
|
'source_type' => 'kemendik',
|
||||||
|
'nmmhsmsmh' => $row['nama'],
|
||||||
|
'tahun_lulus' => $row['tahun_lulus'] ?? null,
|
||||||
|
'f5c_jabatan_kode' => $raw['F5c'] ?? null,
|
||||||
|
'f5c_jabatan_text' => $this->mapF5c($raw['F5c'] ?? null),
|
||||||
|
'f1101_jenis_instansi_kode' => $raw['F1101'] ?? null,
|
||||||
|
'f1101_jenis_instansi_text' => $this->mapF1101($raw['F1101'] ?? null),
|
||||||
|
'f5a1_provinsi' => $raw['F5a1'] ?? null,
|
||||||
|
'raw_data' => json_encode($raw)
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
$internalResultsUpsertData[] = [
|
||||||
|
'nim' => $row['nim'],
|
||||||
|
'source_type' => 'internal_mif',
|
||||||
|
'nama' => $row['nama'],
|
||||||
|
'tahun_lulus' => $row['tahun_lulus'] ?? null,
|
||||||
|
'job_text_raw' => $row['job_text_raw'] ?? '',
|
||||||
|
'predicted_profile' => $row['predicted_profile'],
|
||||||
|
'confidence_score' => $row['confidence_score'] ?? 0,
|
||||||
|
'classification_method' => $row['classification_method'],
|
||||||
|
'status' => $row['status'],
|
||||||
|
'error_detail' => $row['error_detail'] ?? null
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isset($row['raw_data'])) {
|
||||||
|
$mapped = ['nim' => $row['nim']];
|
||||||
|
$cols = [
|
||||||
|
'nama_lengkap', 'email', 'no_telepon', 'alamat_domisili', 'jurusan',
|
||||||
|
'program_studi', 'tahun_masuk', 'tahun_lulus', 'status_pekerjaan',
|
||||||
|
'klasifikasi_pekerjaan', 'nama_perusahaan', 'jenis_perusahaan', 'jabatan',
|
||||||
|
'lokasi_perusahaan', 'alamat_perusahaan', 'deskripsi_pekerjaan',
|
||||||
|
'tahun_mulai_kerja', 'masa_kerja_bulan', 'jumlah_lamaran_dikirim',
|
||||||
|
'jumlah_respons_lamaran', 'jumlah_undangan_wawancara', 'masa_tunggu_pra_lulus',
|
||||||
|
'masa_tunggu_pasca_lulus', 'total_masa_tunggu', 'instansi_terupdate',
|
||||||
|
'jabatan_terupdate', 'tahun_bekerja', 'status_pekerjaan_terupdate',
|
||||||
|
'linkedin_profile', 'sosmed_ig'
|
||||||
|
];
|
||||||
|
foreach ($cols as $col) {
|
||||||
|
$val = $row['raw_data'][$col] ?? null;
|
||||||
|
$mapped[$col] = $val === '' ? null : $val;
|
||||||
|
}
|
||||||
|
$internalRawUpsertData[] = $mapped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($kemendikUpsertData)) {
|
||||||
|
foreach (array_chunk($kemendikUpsertData, 500) as $chunk) {
|
||||||
|
KemendikRawData::upsert(
|
||||||
|
$chunk,
|
||||||
|
['nimhsmsmh', 'source_type'],
|
||||||
|
['nmmhsmsmh', 'tahun_lulus', 'f5c_jabatan_kode', 'f5c_jabatan_text', 'f1101_jenis_instansi_kode', 'f1101_jenis_instansi_text', 'f5a1_provinsi', 'raw_data']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($internalResultsUpsertData)) {
|
||||||
|
foreach (array_chunk($internalResultsUpsertData, 500) as $chunk) {
|
||||||
|
ClassificationResult::upsert(
|
||||||
|
$chunk,
|
||||||
|
['nim', 'source_type'],
|
||||||
|
['nama', 'tahun_lulus', 'job_text_raw', 'predicted_profile', 'confidence_score', 'classification_method', 'status', 'error_detail']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($internalRawUpsertData)) {
|
||||||
|
$cols = [
|
||||||
|
'nama_lengkap', 'email', 'no_telepon', 'alamat_domisili', 'jurusan',
|
||||||
|
'program_studi', 'tahun_masuk', 'tahun_lulus', 'status_pekerjaan',
|
||||||
|
'klasifikasi_pekerjaan', 'nama_perusahaan', 'jenis_perusahaan', 'jabatan',
|
||||||
|
'lokasi_perusahaan', 'alamat_perusahaan', 'deskripsi_pekerjaan',
|
||||||
|
'tahun_mulai_kerja', 'masa_kerja_bulan', 'jumlah_lamaran_dikirim',
|
||||||
|
'jumlah_respons_lamaran', 'jumlah_undangan_wawancara', 'masa_tunggu_pra_lulus',
|
||||||
|
'masa_tunggu_pasca_lulus', 'total_masa_tunggu', 'instansi_terupdate',
|
||||||
|
'jabatan_terupdate', 'tahun_bekerja', 'status_pekerjaan_terupdate',
|
||||||
|
'linkedin_profile', 'sosmed_ig'
|
||||||
|
];
|
||||||
|
foreach (array_chunk($internalRawUpsertData, 500) as $chunk) {
|
||||||
|
InternalRawData::upsert($chunk, ['nim'], $cols);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$isKemendik = $result['results'][0]['source_type'] === 'kemendik';
|
||||||
|
if ($isKemendik) {
|
||||||
|
$count = collect($result['results'])->where('source_type','kemendik')->count();
|
||||||
|
return back()->with('success', "Selesai. {$result['processed_rows']} baris diproses. {$count} data Kemendik berhasil masuk.");
|
||||||
|
} else {
|
||||||
|
$classified = collect($result['results'])->where('source_type','internal_mif')->where('status','auto_classified')->count();
|
||||||
|
return back()->with('success', "Selesai. {$result['processed_rows']} data diproses. {$classified} diklasifikasi otomatis (Internal MIF).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mapF5c(?string $c): ?string { return ['1'=>'Founder','2'=>'Co-Founder','3'=>'Staff','4'=>'Freelance'][$c] ?? null; }
|
||||||
|
private function mapF1101(?string $c): ?string { return ['1'=>'Pemerintah','2'=>'Non-profit','3'=>'Swasta','4'=>'Wiraswasta','6'=>'BUMN','7'=>'Multilateral'][$c] ?? null; }
|
||||||
|
|
||||||
|
public function retrain(Request $request)
|
||||||
|
{
|
||||||
|
$overrideData = ClassificationResult::where('source_type', 'internal_mif')
|
||||||
|
->where('status', 'manual_override')
|
||||||
|
->whereNotNull('job_text_raw')
|
||||||
|
->whereNotNull('predicted_profile')
|
||||||
|
->get(['job_text_raw', 'predicted_profile']);
|
||||||
|
|
||||||
|
$csvPath = null;
|
||||||
|
|
||||||
|
if ($overrideData->count() > 0) {
|
||||||
|
$csvLines = ['job_text_raw;label'];
|
||||||
|
foreach ($overrideData as $row) {
|
||||||
|
$text = str_replace([';', "\n", "\r"], [',', ' ', ' '], $row->job_text_raw);
|
||||||
|
$label = $row->predicted_profile;
|
||||||
|
$csvLines[] = "{$text};{$label}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$tmpDir = storage_path('app/temp');
|
||||||
|
if (!is_dir($tmpDir)) {
|
||||||
|
mkdir($tmpDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$csvPath = $tmpDir . '/manual_override_' . Str::uuid() . '.csv';
|
||||||
|
file_put_contents($csvPath, implode("\n", $csvLines));
|
||||||
|
|
||||||
|
Log::info("Retrain: Exported {$overrideData->count()} manual_override rows to {$csvPath}");
|
||||||
|
} else {
|
||||||
|
Log::info('Retrain: Tidak ada data manual_override, training hanya dengan corpus asli.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->fastApi->triggerRetrain($csvPath);
|
||||||
|
|
||||||
|
if ($csvPath && file_exists($csvPath)) {
|
||||||
|
try {
|
||||||
|
unlink($csvPath);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::warning("Gagal menghapus temp CSV: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($result['error'])) {
|
||||||
|
return response()->json(['success' => false, 'message' => $result['error']], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Re-training dimulai di background.',
|
||||||
|
'override_count' => $overrideData->count(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function retrainStatus(Request $request)
|
||||||
|
{
|
||||||
|
$status = $this->fastApi->getRetrainStatus();
|
||||||
|
return response()->json($status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\ClassificationResult;
|
||||||
|
use App\Models\InternalRawData;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class InternalDataController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$stats = [
|
||||||
|
'total_internal' => ClassificationResult::where('source_type', 'internal_mif')->count(),
|
||||||
|
'auto_classified' => ClassificationResult::where('source_type', 'internal_mif')->where('status', 'auto_classified')->count(),
|
||||||
|
'needs_review' => ClassificationResult::where('source_type', 'internal_mif')->where('status', 'needs_review')->count(),
|
||||||
|
];
|
||||||
|
|
||||||
|
$query = ClassificationResult::where('source_type', 'internal_mif');
|
||||||
|
|
||||||
|
$query->orderByRaw("FIELD(status, 'needs_review', 'manual_override', 'auto_classified', 'failed')");
|
||||||
|
|
||||||
|
if ($request->has('sort')) {
|
||||||
|
$allowedSorts = ['nim', 'nama', 'job_text_raw', 'predicted_profile', 'status'];
|
||||||
|
if (in_array($request->sort, $allowedSorts)) {
|
||||||
|
$query->orderBy($request->sort, $request->direction === 'desc' ? 'desc' : 'asc');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$query->latest();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Optimasi OOM: Hitung menggunakan database level, bukan php level mapping ---
|
||||||
|
$chartProfile = ClassificationResult::where('source_type', 'internal_mif')
|
||||||
|
->whereNotNull('predicted_profile')
|
||||||
|
->select('predicted_profile', DB::raw('count(*) as total'))
|
||||||
|
->groupBy('predicted_profile')
|
||||||
|
->pluck('total', 'predicted_profile');
|
||||||
|
|
||||||
|
$chartMethod = ClassificationResult::where('source_type', 'internal_mif')
|
||||||
|
->whereNotNull('classification_method')
|
||||||
|
->select('classification_method', DB::raw('count(*) as total'))
|
||||||
|
->groupBy('classification_method')
|
||||||
|
->pluck('total', 'classification_method');
|
||||||
|
|
||||||
|
// Memakai cursor untuk menghemat memori
|
||||||
|
$waktuTunggu = [];
|
||||||
|
$lamaranDikirim = [];
|
||||||
|
$respons = [];
|
||||||
|
$wawancara = [];
|
||||||
|
$lokasiCounts = [];
|
||||||
|
|
||||||
|
foreach (InternalRawData::cursor() as $r) {
|
||||||
|
$tahun = $r->tahun_lulus;
|
||||||
|
$tunggu = intval($r->total_masa_tunggu ?? 0);
|
||||||
|
if (!empty($tahun) && $tunggu > 0) {
|
||||||
|
$waktuTunggu[$tahun][] = $tunggu;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lamaranDikirim[] = intval($r->jumlah_lamaran_dikirim ?? 0);
|
||||||
|
$respons[] = intval($r->jumlah_respons_lamaran ?? 0);
|
||||||
|
$wawancara[] = intval($r->jumlah_undangan_wawancara ?? 0);
|
||||||
|
|
||||||
|
$lokasi = trim($r->lokasi_perusahaan ?? '');
|
||||||
|
if (!empty($lokasi)) {
|
||||||
|
$lokasiCounts[$lokasi] = ($lokasiCounts[$lokasi] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$chartWaktuTunggu = collect($waktuTunggu)->map(fn($arr) => round(collect($arr)->average(), 1))->sortKeys();
|
||||||
|
|
||||||
|
$chartFunnel = [
|
||||||
|
'Lamaran Dikirim' => count($lamaranDikirim) > 0 ? round(collect($lamaranDikirim)->average(), 1) : 0,
|
||||||
|
'Respons' => count($respons) > 0 ? round(collect($respons)->average(), 1) : 0,
|
||||||
|
'Wawancara' => count($wawancara) > 0 ? round(collect($wawancara)->average(), 1) : 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
$chartLokasi = collect($lokasiCounts);
|
||||||
|
$chartTopLokasi = $chartLokasi->sortDesc()->take(5);
|
||||||
|
|
||||||
|
$charts = [
|
||||||
|
'profile' => $chartProfile,
|
||||||
|
'method' => $chartMethod,
|
||||||
|
'waktu_tunggu' => $chartWaktuTunggu,
|
||||||
|
'funnel' => $chartFunnel,
|
||||||
|
'lokasi' => $chartTopLokasi,
|
||||||
|
'map_lokasi' => $chartLokasi,
|
||||||
|
];
|
||||||
|
|
||||||
|
$metricsPath = base_path('../fastapi/ml_assets/metrics_internal_only.json');
|
||||||
|
$mlMetrics = null;
|
||||||
|
if (file_exists($metricsPath)) {
|
||||||
|
$mlMetrics = json_decode(file_get_contents($metricsPath), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$manualOverrideCount = ClassificationResult::where('source_type', 'internal_mif')
|
||||||
|
->where('status', 'manual_override')
|
||||||
|
->whereNotNull('job_text_raw')
|
||||||
|
->count();
|
||||||
|
|
||||||
|
return view('dashboard', [
|
||||||
|
'stats' => $stats,
|
||||||
|
'charts' => $charts,
|
||||||
|
'internal_data' => $query->paginate(10)->appends($request->query()),
|
||||||
|
'ml_metrics' => $mlMetrics,
|
||||||
|
'manual_override_count' => $manualOverrideCount,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, $id)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'predicted_profile' => 'required|in:Programmer,Data Analyst,Wirausaha Informatika,Non-IT,Tidak Diketahui',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$record = ClassificationResult::findOrFail($id);
|
||||||
|
$record->update([
|
||||||
|
'predicted_profile' => $request->predicted_profile,
|
||||||
|
'status' => 'manual_override'
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('success', 'Data berhasil diperbarui!');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy($id)
|
||||||
|
{
|
||||||
|
$record = ClassificationResult::findOrFail($id);
|
||||||
|
$record->delete();
|
||||||
|
|
||||||
|
return back()->with('success', 'Data berhasil dihapus!');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bulkDestroy(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'ids' => 'required|array',
|
||||||
|
'ids.*' => 'exists:classification_results,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
ClassificationResult::whereIn('id', $request->ids)->delete();
|
||||||
|
|
||||||
|
return back()->with('success', count($request->ids) . ' data berhasil dihapus!');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bulkUpdate(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'ids' => 'required|array',
|
||||||
|
'ids.*' => 'exists:classification_results,id',
|
||||||
|
'predicted_profile' => 'required|in:Programmer,Data Analyst,Wirausaha Informatika,Non-IT,Tidak Diketahui',
|
||||||
|
]);
|
||||||
|
|
||||||
|
ClassificationResult::whereIn('id', $request->ids)->update([
|
||||||
|
'predicted_profile' => $request->predicted_profile,
|
||||||
|
'status' => 'manual_override'
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('success', count($request->ids) . ' data berhasil diubah profilnya!');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exportPdf(Request $request)
|
||||||
|
{
|
||||||
|
$query = ClassificationResult::where('source_type', 'internal_mif');
|
||||||
|
$query->orderByRaw("FIELD(status, 'needs_review', 'manual_override', 'auto_classified', 'failed')");
|
||||||
|
|
||||||
|
if ($request->has('sort')) {
|
||||||
|
$allowedSorts = ['nim', 'nama', 'job_text_raw', 'predicted_profile', 'status'];
|
||||||
|
if (in_array($request->sort, $allowedSorts)) {
|
||||||
|
$query->orderBy($request->sort, $request->direction === 'desc' ? 'desc' : 'asc');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$query->latest();
|
||||||
|
}
|
||||||
|
|
||||||
|
$internal_data = $query->get();
|
||||||
|
$pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('pdf.classification_results', compact('internal_data'));
|
||||||
|
return $pdf->download('Hasil_Klasifikasi_Internal_MIF.pdf');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\KemendikRawData;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class KemendikDataController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$stats = [
|
||||||
|
'total_kemendik' => KemendikRawData::count(),
|
||||||
|
];
|
||||||
|
|
||||||
|
$keteranganPath = base_path('../data/keterangan_ts_kemendiktisaintek.csv');
|
||||||
|
$keterangan = [];
|
||||||
|
if (file_exists($keteranganPath)) {
|
||||||
|
$lines = file($keteranganPath);
|
||||||
|
if (count($lines) > 0) {
|
||||||
|
$rawHeaders = str_getcsv(trim($lines[0]), ';');
|
||||||
|
$headers = array_map('trim', $rawHeaders);
|
||||||
|
|
||||||
|
for ($i = 1; $i < count($lines); $i++) {
|
||||||
|
$row = str_getcsv(trim($lines[$i]), ';');
|
||||||
|
foreach ($headers as $index => $header) {
|
||||||
|
$val = trim($row[$index] ?? '');
|
||||||
|
if ($val !== '') {
|
||||||
|
if (preg_match('/^(\d+)\s*-\s*(.*)/', $val, $matches)) {
|
||||||
|
$keterangan[$header][$matches[1]] = $val;
|
||||||
|
} else {
|
||||||
|
$keterangan[$header][$val] = $val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimasi OOM: Gunakan cursor() alih-alih all()
|
||||||
|
$statusCounts = [];
|
||||||
|
$instansiCounts = [];
|
||||||
|
$kesesuaianCounts = [];
|
||||||
|
$levelCounts = [];
|
||||||
|
$pendapatanRaw = [];
|
||||||
|
$mapLokasi = [];
|
||||||
|
|
||||||
|
foreach (KemendikRawData::cursor() as $r) {
|
||||||
|
$raw = $r->raw_data;
|
||||||
|
if (!$raw) continue;
|
||||||
|
|
||||||
|
$this->incrementCount($statusCounts, $raw, 'F8 (Jelaskan status Anda saat ini?)', $keterangan);
|
||||||
|
$this->incrementCount($instansiCounts, $raw, 'F1101 (Apa jenis perusahaan/instansi/institusi tempat anda bekerja sekarang?)', $keterangan);
|
||||||
|
$this->incrementCount($kesesuaianCounts, $raw, 'F14 (Seberapa erat hubungan bidang studi dengan pekerjaan Anda?)', $keterangan);
|
||||||
|
$this->incrementCount($levelCounts, $raw, 'F5d (Apa tingkatan tempat kerja Anda?)', $keterangan);
|
||||||
|
|
||||||
|
$pendapatanVal = floatval($raw['F505 (Berapa rata-rata pendapatan Anda per bulan?)'] ?? 0);
|
||||||
|
if ($pendapatanVal > 0) {
|
||||||
|
$pendapatanRaw[] = $pendapatanVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lokasi = trim($raw['F5a2 (Dimana lokasi kabupaten/kota tempat Anda bekerja?)'] ?? '');
|
||||||
|
if ($lokasi) {
|
||||||
|
$mapLokasi[$lokasi] = ($mapLokasi[$lokasi] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$chartPendapatan = collect($pendapatanRaw)->reduce(function($carry, $v) {
|
||||||
|
if ($v < 2000000) $carry['< 2 Juta']++;
|
||||||
|
elseif ($v <= 4000000) $carry['2 - 4 Juta']++;
|
||||||
|
elseif ($v <= 6000000) $carry['4 - 6 Juta']++;
|
||||||
|
else $carry['> 6 Juta']++;
|
||||||
|
return $carry;
|
||||||
|
}, ['< 2 Juta' => 0, '2 - 4 Juta' => 0, '4 - 6 Juta' => 0, '> 6 Juta' => 0]);
|
||||||
|
$chartPendapatan = array_filter($chartPendapatan);
|
||||||
|
|
||||||
|
$coordsPath = base_path('../data/kabupaten_coords.json');
|
||||||
|
$kabupatenCoords = file_exists($coordsPath) ? json_decode(file_get_contents($coordsPath), true) : [];
|
||||||
|
|
||||||
|
$charts = [
|
||||||
|
'status' => collect($statusCounts),
|
||||||
|
'instansi' => collect($instansiCounts),
|
||||||
|
'kesesuaian' => collect($kesesuaianCounts),
|
||||||
|
'pendapatan' => $chartPendapatan,
|
||||||
|
'level' => collect($levelCounts),
|
||||||
|
'map_lokasi' => collect($mapLokasi),
|
||||||
|
'coords' => $kabupatenCoords
|
||||||
|
];
|
||||||
|
|
||||||
|
return view('kemendik', [
|
||||||
|
'stats' => $stats,
|
||||||
|
'charts' => $charts,
|
||||||
|
'kemendik_data' => KemendikRawData::orderBy('imported_at', 'desc')->paginate(10),
|
||||||
|
'keterangan' => $keterangan
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function incrementCount(&$countsArray, $raw, $key, $keterangan)
|
||||||
|
{
|
||||||
|
$val = trim($raw[$key] ?? '');
|
||||||
|
if ($val !== '') {
|
||||||
|
$mapped = $keterangan[$key][$val] ?? $val;
|
||||||
|
$countsArray[$mapped] = ($countsArray[$mapped] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,9 +7,5 @@
|
||||||
class InternalRawData extends Model
|
class InternalRawData extends Model
|
||||||
{
|
{
|
||||||
protected $guarded = ['id'];
|
protected $guarded = ['id'];
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'raw_data' => 'array',
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,43 @@
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Http\Client\ConnectionException;
|
||||||
|
|
||||||
class FastApiWorkerService
|
class FastApiWorkerService
|
||||||
{
|
{
|
||||||
private string $baseUrl;
|
private string $baseUrl;
|
||||||
|
private string $secretKey;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->baseUrl = config('services.fastapi.worker_url', 'http://127.0.0.1:8001');
|
$this->baseUrl = config('services.fastapi.worker_url', 'http://127.0.0.1:8001');
|
||||||
|
$this->secretKey = config('services.fastapi.secret_key', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to get HTTP client with common config
|
||||||
|
*/
|
||||||
|
private function getClient(int $timeout = 15)
|
||||||
|
{
|
||||||
|
$client = Http::timeout($timeout);
|
||||||
|
if (!empty($this->secretKey)) {
|
||||||
|
$client = $client->withToken($this->secretKey);
|
||||||
|
}
|
||||||
|
return $client;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function classifyFile(string $filePath, string $filename): array
|
public function classifyFile(string $filePath, string $filename): array
|
||||||
{
|
{
|
||||||
|
$fileStream = null;
|
||||||
try {
|
try {
|
||||||
$response = Http::timeout(config('services.fastapi.timeout', 120))
|
// Fix OOM: Gunakan fopen untuk stream file, bukan file_get_contents
|
||||||
->attach('file', file_get_contents($filePath), $filename)
|
$fileStream = fopen($filePath, 'r');
|
||||||
|
if (!$fileStream) {
|
||||||
|
throw new \Exception("Tidak dapat membaca file: {$filePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->getClient(config('services.fastapi.timeout', 120))
|
||||||
|
->attach('file', $fileStream, $filename)
|
||||||
->post($this->baseUrl . '/api/v1/classify');
|
->post($this->baseUrl . '/api/v1/classify');
|
||||||
|
|
||||||
if ($response->failed()) {
|
if ($response->failed()) {
|
||||||
|
|
@ -26,9 +48,16 @@ public function classifyFile(string $filePath, string $filename): array
|
||||||
return ['error' => "Gagal menghubungi FastAPI: {$response->status()}"];
|
return ['error' => "Gagal menghubungi FastAPI: {$response->status()}"];
|
||||||
}
|
}
|
||||||
return $response->json();
|
return $response->json();
|
||||||
|
} catch (ConnectionException $e) {
|
||||||
|
Log::error("FastAPI Connection Timeout/Refused: {$e->getMessage()}");
|
||||||
|
return ['error' => 'Server AI sedang tidak dapat dijangkau (Timeout/Refused).'];
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error("FastAPI Connection Failed: {$e->getMessage()}");
|
Log::error("FastAPI Internal System Error: {$e->getMessage()}");
|
||||||
return ['error' => "Koneksi ke FastAPI gagal: {$e->getMessage()}"];
|
return ['error' => "Kesalahan sistem internal: {$e->getMessage()}"];
|
||||||
|
} finally {
|
||||||
|
if (is_resource($fileStream)) {
|
||||||
|
fclose($fileStream);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,15 +67,16 @@ public function classifyFile(string $filePath, string $filename): array
|
||||||
*/
|
*/
|
||||||
public function triggerRetrain(?string $csvPath = null): array
|
public function triggerRetrain(?string $csvPath = null): array
|
||||||
{
|
{
|
||||||
|
$fileStream = null;
|
||||||
try {
|
try {
|
||||||
$request = Http::timeout(30);
|
$request = $this->getClient(30);
|
||||||
|
|
||||||
if ($csvPath && file_exists($csvPath)) {
|
if ($csvPath && file_exists($csvPath)) {
|
||||||
$request = $request->attach(
|
$fileStream = fopen($csvPath, 'r');
|
||||||
'file',
|
if (!$fileStream) {
|
||||||
file_get_contents($csvPath),
|
throw new \Exception("Tidak dapat membaca temp CSV: {$csvPath}");
|
||||||
'manual_override.csv'
|
}
|
||||||
);
|
$request = $request->attach('file', $fileStream, 'manual_override.csv');
|
||||||
}
|
}
|
||||||
|
|
||||||
$response = $request->post($this->baseUrl . '/api/v1/retrain');
|
$response = $request->post($this->baseUrl . '/api/v1/retrain');
|
||||||
|
|
@ -61,9 +91,16 @@ public function triggerRetrain(?string $csvPath = null): array
|
||||||
}
|
}
|
||||||
|
|
||||||
return $response->json();
|
return $response->json();
|
||||||
|
} catch (ConnectionException $e) {
|
||||||
|
Log::error("FastAPI Retrain Connection Timeout/Refused: {$e->getMessage()}");
|
||||||
|
return ['error' => 'Server AI sedang tidak dapat dijangkau (Timeout/Refused).'];
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error("FastAPI Retrain Failed: {$e->getMessage()}");
|
Log::error("FastAPI Retrain Internal System Error: {$e->getMessage()}");
|
||||||
return ['error' => "Koneksi ke FastAPI gagal: {$e->getMessage()}"];
|
return ['error' => "Kesalahan sistem internal: {$e->getMessage()}"];
|
||||||
|
} finally {
|
||||||
|
if (is_resource($fileStream)) {
|
||||||
|
fclose($fileStream);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,16 +110,19 @@ public function triggerRetrain(?string $csvPath = null): array
|
||||||
public function getRetrainStatus(): array
|
public function getRetrainStatus(): array
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$response = Http::timeout(10)->get($this->baseUrl . '/api/v1/retrain/status');
|
$response = $this->getClient(10)->get($this->baseUrl . '/api/v1/retrain/status');
|
||||||
|
|
||||||
if ($response->failed()) {
|
if ($response->failed()) {
|
||||||
return ['stage' => 'unknown', 'message' => 'Tidak dapat membaca status dari FastAPI.'];
|
return ['stage' => 'unknown', 'message' => 'Tidak dapat membaca status dari FastAPI.'];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $response->json();
|
return $response->json();
|
||||||
|
} catch (ConnectionException $e) {
|
||||||
|
Log::warning("FastAPI Status Check Connection Refused/Timeout: {$e->getMessage()}");
|
||||||
|
return ['stage' => 'unknown', 'message' => 'Server AI tidak merespons (Timeout).'];
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::warning("FastAPI Status Check Failed: {$e->getMessage()}");
|
Log::warning("FastAPI Status Check System Error: {$e->getMessage()}");
|
||||||
return ['stage' => 'unknown', 'message' => "Koneksi gagal: {$e->getMessage()}"];
|
return ['stage' => 'unknown', 'message' => "Kesalahan sistem internal: {$e->getMessage()}"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,16 +132,19 @@ public function getRetrainStatus(): array
|
||||||
public function reloadModel(): array
|
public function reloadModel(): array
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$response = Http::timeout(15)->post($this->baseUrl . '/api/v1/retrain/reload');
|
$response = $this->getClient(15)->post($this->baseUrl . '/api/v1/retrain/reload');
|
||||||
|
|
||||||
if ($response->failed()) {
|
if ($response->failed()) {
|
||||||
return ['error' => "Gagal reload model: HTTP {$response->status()}"];
|
return ['error' => "Gagal reload model: HTTP {$response->status()}"];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $response->json();
|
return $response->json();
|
||||||
|
} catch (ConnectionException $e) {
|
||||||
|
Log::error("FastAPI Reload Connection Timeout/Refused: {$e->getMessage()}");
|
||||||
|
return ['error' => 'Server AI sedang tidak dapat dijangkau (Timeout/Refused).'];
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error("FastAPI Reload Failed: {$e->getMessage()}");
|
Log::error("FastAPI Reload System Error: {$e->getMessage()}");
|
||||||
return ['error' => "Koneksi ke FastAPI gagal: {$e->getMessage()}"];
|
return ['error' => "Kesalahan sistem internal: {$e->getMessage()}"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7,6 +7,8 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.3",
|
"php": "^8.3",
|
||||||
|
"barryvdh/laravel-dompdf": "^3.1",
|
||||||
|
"guzzlehttp/guzzle": "^7.10",
|
||||||
"laravel/framework": "^13.8",
|
"laravel/framework": "^13.8",
|
||||||
"laravel/tinker": "^3.0"
|
"laravel/tinker": "^3.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,85 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "efc013d7b1ca50dea035fa69d50022c4",
|
"content-hash": "dd902983f1193a2e136efdb6349c690e",
|
||||||
"packages": [
|
"packages": [
|
||||||
|
{
|
||||||
|
"name": "barryvdh/laravel-dompdf",
|
||||||
|
"version": "v3.1.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/barryvdh/laravel-dompdf.git",
|
||||||
|
"reference": "ee3b72b19ccdf57d0243116ecb2b90261344dedc"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/ee3b72b19ccdf57d0243116ecb2b90261344dedc",
|
||||||
|
"reference": "ee3b72b19ccdf57d0243116ecb2b90261344dedc",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"dompdf/dompdf": "^3.0",
|
||||||
|
"illuminate/support": "^9|^10|^11|^12|^13.0",
|
||||||
|
"php": "^8.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"larastan/larastan": "^2.7|^3.0",
|
||||||
|
"orchestra/testbench": "^7|^8|^9.16|^10|^11.0",
|
||||||
|
"phpro/grumphp": "^2.5",
|
||||||
|
"squizlabs/php_codesniffer": "^3.5"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"aliases": {
|
||||||
|
"PDF": "Barryvdh\\DomPDF\\Facade\\Pdf",
|
||||||
|
"Pdf": "Barryvdh\\DomPDF\\Facade\\Pdf"
|
||||||
|
},
|
||||||
|
"providers": [
|
||||||
|
"Barryvdh\\DomPDF\\ServiceProvider"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "3.0-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Barryvdh\\DomPDF\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Barry vd. Heuvel",
|
||||||
|
"email": "barryvdh@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A DOMPDF Wrapper for Laravel",
|
||||||
|
"keywords": [
|
||||||
|
"dompdf",
|
||||||
|
"laravel",
|
||||||
|
"pdf"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/barryvdh/laravel-dompdf/issues",
|
||||||
|
"source": "https://github.com/barryvdh/laravel-dompdf/tree/v3.1.2"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://fruitcake.nl",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/barryvdh",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-02-21T08:51:10+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
"version": "0.14.8",
|
"version": "0.14.8",
|
||||||
|
|
@ -377,6 +454,161 @@
|
||||||
],
|
],
|
||||||
"time": "2024-02-05T11:56:58+00:00"
|
"time": "2024-02-05T11:56:58+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "dompdf/dompdf",
|
||||||
|
"version": "v3.1.5",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/dompdf/dompdf.git",
|
||||||
|
"reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/dompdf/dompdf/zipball/f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496",
|
||||||
|
"reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"dompdf/php-font-lib": "^1.0.0",
|
||||||
|
"dompdf/php-svg-lib": "^1.0.0",
|
||||||
|
"ext-dom": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"masterminds/html5": "^2.0",
|
||||||
|
"php": "^7.1 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"ext-gd": "*",
|
||||||
|
"ext-json": "*",
|
||||||
|
"ext-zip": "*",
|
||||||
|
"mockery/mockery": "^1.3",
|
||||||
|
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11",
|
||||||
|
"squizlabs/php_codesniffer": "^3.5",
|
||||||
|
"symfony/process": "^4.4 || ^5.4 || ^6.2 || ^7.0"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-gd": "Needed to process images",
|
||||||
|
"ext-gmagick": "Improves image processing performance",
|
||||||
|
"ext-imagick": "Improves image processing performance",
|
||||||
|
"ext-zlib": "Needed for pdf stream compression"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Dompdf\\": "src/"
|
||||||
|
},
|
||||||
|
"classmap": [
|
||||||
|
"lib/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"LGPL-2.1"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "The Dompdf Community",
|
||||||
|
"homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter",
|
||||||
|
"homepage": "https://github.com/dompdf/dompdf",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/dompdf/dompdf/issues",
|
||||||
|
"source": "https://github.com/dompdf/dompdf/tree/v3.1.5"
|
||||||
|
},
|
||||||
|
"time": "2026-03-03T13:54:37+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dompdf/php-font-lib",
|
||||||
|
"version": "1.0.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/dompdf/php-font-lib.git",
|
||||||
|
"reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/a6e9a688a2a80016ac080b97be73d3e10c444c9a",
|
||||||
|
"reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"php": "^7.1 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11 || ^12"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"FontLib\\": "src/FontLib"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"LGPL-2.1-or-later"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "The FontLib Community",
|
||||||
|
"homepage": "https://github.com/dompdf/php-font-lib/blob/master/AUTHORS.md"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A library to read, parse, export and make subsets of different types of font files.",
|
||||||
|
"homepage": "https://github.com/dompdf/php-font-lib",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/dompdf/php-font-lib/issues",
|
||||||
|
"source": "https://github.com/dompdf/php-font-lib/tree/1.0.2"
|
||||||
|
},
|
||||||
|
"time": "2026-01-20T14:10:26+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dompdf/php-svg-lib",
|
||||||
|
"version": "1.0.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/dompdf/php-svg-lib.git",
|
||||||
|
"reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/8259ffb930817e72b1ff1caef5d226501f3dfeb1",
|
||||||
|
"reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"php": "^7.1 || ^8.0",
|
||||||
|
"sabberworm/php-css-parser": "^8.4 || ^9.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Svg\\": "src/Svg"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"LGPL-3.0-or-later"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "The SvgLib Community",
|
||||||
|
"homepage": "https://github.com/dompdf/php-svg-lib/blob/master/AUTHORS.md"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A library to read, parse and export to PDF SVG files.",
|
||||||
|
"homepage": "https://github.com/dompdf/php-svg-lib",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/dompdf/php-svg-lib/issues",
|
||||||
|
"source": "https://github.com/dompdf/php-svg-lib/tree/1.0.2"
|
||||||
|
},
|
||||||
|
"time": "2026-01-02T16:01:13+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "dragonmantank/cron-expression",
|
"name": "dragonmantank/cron-expression",
|
||||||
"version": "v3.6.0",
|
"version": "v3.6.0",
|
||||||
|
|
@ -2026,6 +2258,73 @@
|
||||||
],
|
],
|
||||||
"time": "2026-03-08T20:05:35+00:00"
|
"time": "2026-03-08T20:05:35+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "masterminds/html5",
|
||||||
|
"version": "2.10.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Masterminds/html5-php.git",
|
||||||
|
"reference": "fcf91eb64359852f00d921887b219479b4f21251"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251",
|
||||||
|
"reference": "fcf91eb64359852f00d921887b219479b4f21251",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-dom": "*",
|
||||||
|
"php": ">=5.3.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "2.7-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Masterminds\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Matt Butcher",
|
||||||
|
"email": "technosophos@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Matt Farina",
|
||||||
|
"email": "matt@mattfarina.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Asmir Mustafic",
|
||||||
|
"email": "goetas@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "An HTML5 parser and serializer.",
|
||||||
|
"homepage": "http://masterminds.github.io/html5-php",
|
||||||
|
"keywords": [
|
||||||
|
"HTML5",
|
||||||
|
"dom",
|
||||||
|
"html",
|
||||||
|
"parser",
|
||||||
|
"querypath",
|
||||||
|
"serializer",
|
||||||
|
"xml"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/Masterminds/html5-php/issues",
|
||||||
|
"source": "https://github.com/Masterminds/html5-php/tree/2.10.0"
|
||||||
|
},
|
||||||
|
"time": "2025-07-25T09:04:22+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "monolog/monolog",
|
"name": "monolog/monolog",
|
||||||
"version": "3.10.0",
|
"version": "3.10.0",
|
||||||
|
|
@ -3301,6 +3600,86 @@
|
||||||
},
|
},
|
||||||
"time": "2025-12-14T04:43:48+00:00"
|
"time": "2025-12-14T04:43:48+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "sabberworm/php-css-parser",
|
||||||
|
"version": "v9.3.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/MyIntervals/PHP-CSS-Parser.git",
|
||||||
|
"reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949",
|
||||||
|
"reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-iconv": "*",
|
||||||
|
"php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
|
||||||
|
"thecodingmachine/safe": "^1.3 || ^2.5 || ^3.4"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"php-parallel-lint/php-parallel-lint": "1.4.0",
|
||||||
|
"phpstan/extension-installer": "1.4.3",
|
||||||
|
"phpstan/phpstan": "1.12.32 || 2.1.32",
|
||||||
|
"phpstan/phpstan-phpunit": "1.4.2 || 2.0.8",
|
||||||
|
"phpstan/phpstan-strict-rules": "1.6.2 || 2.0.7",
|
||||||
|
"phpunit/phpunit": "8.5.52",
|
||||||
|
"rawr/phpunit-data-provider": "3.3.1",
|
||||||
|
"rector/rector": "1.2.10 || 2.2.8",
|
||||||
|
"rector/type-perfect": "1.0.0 || 2.1.0",
|
||||||
|
"squizlabs/php_codesniffer": "4.0.1",
|
||||||
|
"thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.1"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-mbstring": "for parsing UTF-8 CSS"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-main": "9.4.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"src/Rule/Rule.php",
|
||||||
|
"src/RuleSet/RuleContainer.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Sabberworm\\CSS\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Raphael Schweikert"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Oliver Klee",
|
||||||
|
"email": "github@oliverklee.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jake Hotson",
|
||||||
|
"email": "jake.github@qzdesign.co.uk"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Parser for CSS Files written in PHP",
|
||||||
|
"homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser",
|
||||||
|
"keywords": [
|
||||||
|
"css",
|
||||||
|
"parser",
|
||||||
|
"stylesheet"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues",
|
||||||
|
"source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.3.0"
|
||||||
|
},
|
||||||
|
"time": "2026-03-03T17:31:43+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/clock",
|
"name": "symfony/clock",
|
||||||
"version": "v7.4.8",
|
"version": "v7.4.8",
|
||||||
|
|
@ -5890,6 +6269,149 @@
|
||||||
],
|
],
|
||||||
"time": "2026-03-30T13:44:50+00:00"
|
"time": "2026-03-30T13:44:50+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "thecodingmachine/safe",
|
||||||
|
"version": "v3.4.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/thecodingmachine/safe.git",
|
||||||
|
"reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19",
|
||||||
|
"reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^8.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"php-parallel-lint/php-parallel-lint": "^1.4",
|
||||||
|
"phpstan/phpstan": "^2",
|
||||||
|
"phpunit/phpunit": "^10",
|
||||||
|
"squizlabs/php_codesniffer": "^3.2"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"lib/special_cases.php",
|
||||||
|
"generated/apache.php",
|
||||||
|
"generated/apcu.php",
|
||||||
|
"generated/array.php",
|
||||||
|
"generated/bzip2.php",
|
||||||
|
"generated/calendar.php",
|
||||||
|
"generated/classobj.php",
|
||||||
|
"generated/com.php",
|
||||||
|
"generated/cubrid.php",
|
||||||
|
"generated/curl.php",
|
||||||
|
"generated/datetime.php",
|
||||||
|
"generated/dir.php",
|
||||||
|
"generated/eio.php",
|
||||||
|
"generated/errorfunc.php",
|
||||||
|
"generated/exec.php",
|
||||||
|
"generated/fileinfo.php",
|
||||||
|
"generated/filesystem.php",
|
||||||
|
"generated/filter.php",
|
||||||
|
"generated/fpm.php",
|
||||||
|
"generated/ftp.php",
|
||||||
|
"generated/funchand.php",
|
||||||
|
"generated/gettext.php",
|
||||||
|
"generated/gmp.php",
|
||||||
|
"generated/gnupg.php",
|
||||||
|
"generated/hash.php",
|
||||||
|
"generated/ibase.php",
|
||||||
|
"generated/ibmDb2.php",
|
||||||
|
"generated/iconv.php",
|
||||||
|
"generated/image.php",
|
||||||
|
"generated/imap.php",
|
||||||
|
"generated/info.php",
|
||||||
|
"generated/inotify.php",
|
||||||
|
"generated/json.php",
|
||||||
|
"generated/ldap.php",
|
||||||
|
"generated/libxml.php",
|
||||||
|
"generated/lzf.php",
|
||||||
|
"generated/mailparse.php",
|
||||||
|
"generated/mbstring.php",
|
||||||
|
"generated/misc.php",
|
||||||
|
"generated/mysql.php",
|
||||||
|
"generated/mysqli.php",
|
||||||
|
"generated/network.php",
|
||||||
|
"generated/oci8.php",
|
||||||
|
"generated/opcache.php",
|
||||||
|
"generated/openssl.php",
|
||||||
|
"generated/outcontrol.php",
|
||||||
|
"generated/pcntl.php",
|
||||||
|
"generated/pcre.php",
|
||||||
|
"generated/pgsql.php",
|
||||||
|
"generated/posix.php",
|
||||||
|
"generated/ps.php",
|
||||||
|
"generated/pspell.php",
|
||||||
|
"generated/readline.php",
|
||||||
|
"generated/rnp.php",
|
||||||
|
"generated/rpminfo.php",
|
||||||
|
"generated/rrd.php",
|
||||||
|
"generated/sem.php",
|
||||||
|
"generated/session.php",
|
||||||
|
"generated/shmop.php",
|
||||||
|
"generated/sockets.php",
|
||||||
|
"generated/sodium.php",
|
||||||
|
"generated/solr.php",
|
||||||
|
"generated/spl.php",
|
||||||
|
"generated/sqlsrv.php",
|
||||||
|
"generated/ssdeep.php",
|
||||||
|
"generated/ssh2.php",
|
||||||
|
"generated/stream.php",
|
||||||
|
"generated/strings.php",
|
||||||
|
"generated/swoole.php",
|
||||||
|
"generated/uodbc.php",
|
||||||
|
"generated/uopz.php",
|
||||||
|
"generated/url.php",
|
||||||
|
"generated/var.php",
|
||||||
|
"generated/xdiff.php",
|
||||||
|
"generated/xml.php",
|
||||||
|
"generated/xmlrpc.php",
|
||||||
|
"generated/yaml.php",
|
||||||
|
"generated/yaz.php",
|
||||||
|
"generated/zip.php",
|
||||||
|
"generated/zlib.php"
|
||||||
|
],
|
||||||
|
"classmap": [
|
||||||
|
"lib/DateTime.php",
|
||||||
|
"lib/DateTimeImmutable.php",
|
||||||
|
"lib/Exceptions/",
|
||||||
|
"generated/Exceptions/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"description": "PHP core functions that throw exceptions instead of returning FALSE on error",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/thecodingmachine/safe/issues",
|
||||||
|
"source": "https://github.com/thecodingmachine/safe/tree/v3.4.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/OskarStark",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/shish",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/silasjoisten",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/staabm",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-02-04T18:08:13+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "tijsverkoyen/css-to-inline-styles",
|
"name": "tijsverkoyen/css-to-inline-styles",
|
||||||
"version": "v2.4.0",
|
"version": "v2.4.0",
|
||||||
|
|
|
||||||
|
|
@ -35,4 +35,10 @@
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'fastapi' => [
|
||||||
|
'worker_url' => env('FASTAPI_WORKER_URL', 'http://127.0.0.1:8001'),
|
||||||
|
'secret_key' => env('FASTAPI_SECRET_KEY'),
|
||||||
|
'timeout' => env('FASTAPI_TIMEOUT', 120),
|
||||||
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ public function up(): void
|
||||||
$table->string('tahun_lulus')->nullable();
|
$table->string('tahun_lulus')->nullable();
|
||||||
$table->text('job_text_raw');
|
$table->text('job_text_raw');
|
||||||
$table->enum('source_type', ['internal_mif', 'kemendik'])->default('internal_mif');
|
$table->enum('source_type', ['internal_mif', 'kemendik'])->default('internal_mif');
|
||||||
$table->enum('predicted_profile', ['Programmer', 'Data Analyst', 'Wirausaha Informatika', 'Non-IT'])->nullable();
|
$table->string('predicted_profile')->nullable();
|
||||||
$table->decimal('confidence_score', 5, 4)->nullable();
|
$table->decimal('confidence_score', 5, 4)->nullable();
|
||||||
$table->string('classification_method')->nullable();
|
$table->string('classification_method')->nullable();
|
||||||
$table->enum('status', ['auto_classified', 'needs_review', 'manual_override', 'failed'])->default('needs_review');
|
$table->enum('status', ['auto_classified', 'needs_review', 'manual_override', 'failed'])->default('needs_review');
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,37 @@ public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('internal_raw_data', function (Blueprint $table) {
|
Schema::create('internal_raw_data', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
|
$table->string('nama_lengkap')->nullable();
|
||||||
$table->string('nim')->index();
|
$table->string('nim')->index();
|
||||||
$table->json('raw_data')->nullable();
|
$table->string('email')->nullable();
|
||||||
|
$table->string('no_telepon')->nullable();
|
||||||
|
$table->text('alamat_domisili')->nullable();
|
||||||
|
$table->string('jurusan')->nullable();
|
||||||
|
$table->string('program_studi')->nullable();
|
||||||
|
$table->string('tahun_masuk')->nullable();
|
||||||
|
$table->string('tahun_lulus')->nullable();
|
||||||
|
$table->string('status_pekerjaan')->nullable();
|
||||||
|
$table->string('klasifikasi_pekerjaan')->nullable();
|
||||||
|
$table->string('nama_perusahaan')->nullable();
|
||||||
|
$table->string('jenis_perusahaan')->nullable();
|
||||||
|
$table->string('jabatan')->nullable();
|
||||||
|
$table->string('lokasi_perusahaan')->nullable();
|
||||||
|
$table->text('alamat_perusahaan')->nullable();
|
||||||
|
$table->text('deskripsi_pekerjaan')->nullable();
|
||||||
|
$table->string('tahun_mulai_kerja')->nullable();
|
||||||
|
$table->string('masa_kerja_bulan')->nullable();
|
||||||
|
$table->string('jumlah_lamaran_dikirim')->nullable();
|
||||||
|
$table->string('jumlah_respons_lamaran')->nullable();
|
||||||
|
$table->string('jumlah_undangan_wawancara')->nullable();
|
||||||
|
$table->string('masa_tunggu_pra_lulus')->nullable();
|
||||||
|
$table->string('masa_tunggu_pasca_lulus')->nullable();
|
||||||
|
$table->string('total_masa_tunggu')->nullable();
|
||||||
|
$table->string('instansi_terupdate')->nullable();
|
||||||
|
$table->string('jabatan_terupdate')->nullable();
|
||||||
|
$table->string('tahun_bekerja')->nullable();
|
||||||
|
$table->string('status_pekerjaan_terupdate')->nullable();
|
||||||
|
$table->string('linkedin_profile')->nullable();
|
||||||
|
$table->string('sosmed_ig')->nullable();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,29 +29,6 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/core": {
|
|
||||||
"version": "1.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
|
||||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@emnapi/wasi-threads": "1.2.1",
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@emnapi/runtime": {
|
|
||||||
"version": "1.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
|
||||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@emnapi/wasi-threads": {
|
"node_modules/@emnapi/wasi-threads": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||||
|
|
@ -931,6 +908,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.10.12",
|
"baseline-browser-mapping": "^2.10.12",
|
||||||
"caniuse-lite": "^1.0.30001782",
|
"caniuse-lite": "^1.0.30001782",
|
||||||
|
|
@ -1916,6 +1894,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.12",
|
"nanoid": "^3.3.12",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
|
|
@ -2323,6 +2302,7 @@
|
||||||
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
"arg": "^5.0.2",
|
"arg": "^5.0.2",
|
||||||
|
|
@ -2443,6 +2423,7 @@
|
||||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|
@ -2531,6 +2512,7 @@
|
||||||
"integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==",
|
"integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"picomatch": "^4.0.4",
|
"picomatch": "^4.0.4",
|
||||||
|
|
|
||||||
|
|
@ -421,6 +421,7 @@ class="inline-flex items-center gap-2 px-6 py-3 rounded-xl text-sm font-semibold
|
||||||
@php
|
@php
|
||||||
$colorClass = match($row->predicted_profile) {
|
$colorClass = match($row->predicted_profile) {
|
||||||
'Programmer' => 'bg-emerald-100 text-emerald-700 border-emerald-200',
|
'Programmer' => 'bg-emerald-100 text-emerald-700 border-emerald-200',
|
||||||
|
'Tidak Diketahui' => 'bg-slate-100 text-slate-500 border-slate-200',
|
||||||
'Non-IT' => 'bg-slate-100 text-slate-700 border-slate-200',
|
'Non-IT' => 'bg-slate-100 text-slate-700 border-slate-200',
|
||||||
default => 'bg-blue-100 text-blue-700 border-blue-200',
|
default => 'bg-blue-100 text-blue-700 border-blue-200',
|
||||||
};
|
};
|
||||||
|
|
@ -459,6 +460,57 @@ class="inline-flex items-center gap-2 px-6 py-3 rounded-xl text-sm font-semibold
|
||||||
@method('DELETE')
|
@method('DELETE')
|
||||||
<button type="submit" class="text-rose-600 hover:text-rose-900 transition-colors">Hapus</button>
|
<button type="submit" class="text-rose-600 hover:text-rose-900 transition-colors">Hapus</button>
|
||||||
</form>
|
</form>
|
||||||
|
<div x-data="{ openDetail: false }" class="inline-block text-left ml-2">
|
||||||
|
<button @click="openDetail = true" class="text-purple-600 hover:text-purple-900 transition-colors">Detail</button>
|
||||||
|
|
||||||
|
<!-- Modal Detail -->
|
||||||
|
<div x-show="openDetail" style="display: none;" class="fixed inset-0 z-[70] overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||||
|
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
|
<div x-show="openDetail" @click="openDetail = false" x-transition.opacity class="fixed inset-0 bg-slate-900 bg-opacity-50 transition-opacity" aria-hidden="true"></div>
|
||||||
|
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||||
|
<div x-show="openDetail" x-transition class="inline-block align-bottom bg-white rounded-2xl text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-3xl w-full">
|
||||||
|
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4 border-b border-slate-100">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h3 class="text-lg leading-6 font-bold text-slate-800" id="modal-title">
|
||||||
|
Detail Data Internal - {{ $row->nama }}
|
||||||
|
</h3>
|
||||||
|
<button @click="openDetail = false" class="text-slate-400 hover:text-slate-600 focus:outline-none">
|
||||||
|
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="max-h-[60vh] overflow-y-auto">
|
||||||
|
<table class="min-w-full divide-y divide-slate-200">
|
||||||
|
<tbody class="bg-white divide-y divide-slate-100">
|
||||||
|
@php
|
||||||
|
$raw = \App\Models\InternalRawData::where('nim', $row->nim)->first();
|
||||||
|
@endphp
|
||||||
|
@if($raw)
|
||||||
|
@foreach($raw->getAttributes() as $key => $val)
|
||||||
|
@if(!in_array($key, ['id', 'created_at', 'updated_at']) && $val !== null && $val !== '')
|
||||||
|
<tr>
|
||||||
|
<td class="px-4 py-3 text-sm font-medium text-slate-700 w-1/3 capitalize whitespace-normal">{{ str_replace('_', ' ', $key) }}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-slate-600 w-2/3 whitespace-normal">{{ $val }}</td>
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
@else
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" class="px-4 py-3 text-sm text-slate-500 text-center">Data detail tidak tersedia.</td>
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-slate-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse border-t border-slate-100">
|
||||||
|
<button type="button" @click="openDetail = false" class="mt-3 w-full inline-flex justify-center rounded-xl border border-slate-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-slate-700 hover:bg-slate-50 focus:outline-none sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
|
||||||
|
Tutup
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@empty
|
@empty
|
||||||
|
|
@ -508,6 +560,7 @@ class="inline-flex items-center gap-2 px-6 py-3 rounded-xl text-sm font-semibold
|
||||||
<option value="Data Analyst">Data Analyst</option>
|
<option value="Data Analyst">Data Analyst</option>
|
||||||
<option value="Wirausaha Informatika">Wirausaha Informatika</option>
|
<option value="Wirausaha Informatika">Wirausaha Informatika</option>
|
||||||
<option value="Non-IT">Non-IT</option>
|
<option value="Non-IT">Non-IT</option>
|
||||||
|
<option value="Tidak Diketahui">Tidak Diketahui</option>
|
||||||
</select>
|
</select>
|
||||||
<p class="mt-2 text-xs text-slate-400">Status akan otomatis berubah menjadi <span class="font-semibold text-blue-600">Manual</span> setelah diperbarui.</p>
|
<p class="mt-2 text-xs text-slate-400">Status akan otomatis berubah menjadi <span class="font-semibold text-blue-600">Manual</span> setelah diperbarui.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,30 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\ProfileController;
|
use App\Http\Controllers\ProfileController;
|
||||||
use App\Http\Controllers\ClassificationController;
|
use App\Http\Controllers\InternalDataController;
|
||||||
|
use App\Http\Controllers\KemendikDataController;
|
||||||
|
use App\Http\Controllers\ClassificationTaskController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('/', function () { return view('auth.login'); });
|
Route::get('/', function () { return view('auth.login'); });
|
||||||
|
|
||||||
Route::middleware(['auth'])->group(function () {
|
Route::middleware(['auth'])->group(function () {
|
||||||
Route::get('/dashboard', [ClassificationController::class, 'dashboard'])->name('dashboard');
|
Route::get('/dashboard', [InternalDataController::class, 'index'])->name('dashboard');
|
||||||
Route::get('/kemendik', [ClassificationController::class, 'kemendik'])->name('kemendik');
|
Route::get('/kemendik', [KemendikDataController::class, 'index'])->name('kemendik');
|
||||||
Route::post('/upload', [ClassificationController::class, 'upload'])->name('upload.process');
|
|
||||||
Route::put('/internal/{id}', [ClassificationController::class, 'update'])->name('internal.update');
|
// Import Data
|
||||||
Route::delete('/internal/{id}', [ClassificationController::class, 'destroy'])->name('internal.destroy');
|
Route::post('/upload', [ClassificationTaskController::class, 'upload'])->name('upload.process');
|
||||||
Route::post('/internal/bulk-delete', [ClassificationController::class, 'bulkDestroy'])->name('internal.bulk_destroy');
|
|
||||||
Route::post('/internal/bulk-update', [ClassificationController::class, 'bulkUpdate'])->name('internal.bulk_update');
|
// Internal Data Management
|
||||||
Route::get('/internal/download-pdf', [ClassificationController::class, 'downloadPdf'])->name('internal.download_pdf');
|
Route::put('/internal/{id}', [InternalDataController::class, 'update'])->name('internal.update');
|
||||||
Route::post('/retrain', [ClassificationController::class, 'retrain'])->name('retrain.trigger');
|
Route::delete('/internal/{id}', [InternalDataController::class, 'destroy'])->name('internal.destroy');
|
||||||
Route::get('/retrain/status', [ClassificationController::class, 'retrainStatus'])->name('retrain.status');
|
Route::post('/internal/bulk-delete', [InternalDataController::class, 'bulkDestroy'])->name('internal.bulk_destroy');
|
||||||
|
Route::post('/internal/bulk-update', [InternalDataController::class, 'bulkUpdate'])->name('internal.bulk_update');
|
||||||
|
Route::get('/internal/download-pdf', [InternalDataController::class, 'exportPdf'])->name('internal.download_pdf');
|
||||||
|
|
||||||
|
// ML Task
|
||||||
|
Route::post('/retrain', [ClassificationTaskController::class, 'retrain'])->name('retrain.trigger');
|
||||||
|
Route::get('/retrain/status', [ClassificationTaskController::class, 'retrainStatus'])->name('retrain.status');
|
||||||
|
|
||||||
|
|
||||||
Route::prefix('profile')->group(function () {
|
Route::prefix('profile')->group(function () {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue