TIF_E41253310/Backend/app/routers/dashboard.py

540 lines
20 KiB
Python

"""
Router untuk Dashboard API - PRD Section 22
"""
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from sqlalchemy import func, or_
from typing import Optional, List, Dict, Any
from app.config.database import get_db
from app.models.alumni import Alumni
from app.models.models import Pekerjaan, Kompetensi, ClusteringResult, ModelEvaluation, TracerResponse
router = APIRouter(tags=["Dashboard"])
@router.get("/stats")
def get_dashboard_stats(
kode_prodi: Optional[str] = Query(None),
tahun_lulus: Optional[int] = Query(None),
db: Session = Depends(get_db)
):
"""
Dashboard Utama - PRD Section 22
Menampilkan: total alumni, total bekerja, total belum bekerja, distribusi C0-C3
Mendukung filter prodi (khusus role Kaprodi atau Admin) dan filter angkatan/tahun_lulus
"""
prodi_filter = kode_prodi if (kode_prodi and kode_prodi != "all") else None
# Total alumni
q_alumni = db.query(Alumni)
if prodi_filter:
q_alumni = q_alumni.filter(Alumni.kode_prodi == prodi_filter)
if tahun_lulus:
q_alumni = q_alumni.filter(Alumni.tahun_lulus == int(tahun_lulus))
total_alumni = q_alumni.count()
# Total hasil clustering
q_cluster = db.query(ClusteringResult).join(Alumni, Alumni.id == ClusteringResult.alumni_id)
if prodi_filter:
q_cluster = q_cluster.filter(Alumni.kode_prodi == prodi_filter)
if tahun_lulus:
q_cluster = q_cluster.filter(Alumni.tahun_lulus == int(tahun_lulus))
total_clustered = q_cluster.count()
# Distribusi kategori C0-C3
q_kategori = db.query(
ClusteringResult.kategori,
func.count(ClusteringResult.id).label('count')
).join(Alumni, Alumni.id == ClusteringResult.alumni_id)
if prodi_filter:
q_kategori = q_kategori.filter(Alumni.kode_prodi == prodi_filter)
if tahun_lulus:
q_kategori = q_kategori.filter(Alumni.tahun_lulus == int(tahun_lulus))
kategori_dist = q_kategori.group_by(ClusteringResult.kategori).all()
kategori_dict = {k: c for k, c in kategori_dist}
# Ambil model evaluation terbaru
latest_eval = db.query(ModelEvaluation).order_by(
ModelEvaluation.created_at.desc()
).first()
# Get unique tahun lulus
q_tahun = db.query(Alumni.tahun_lulus)
if prodi_filter:
q_tahun = q_tahun.filter(Alumni.kode_prodi == prodi_filter)
tahun_list = q_tahun.distinct().order_by(Alumni.tahun_lulus.desc()).all()
tahun_lulus = [t[0] for t in tahun_list if t[0]]
return {
"total_alumni": total_alumni,
"total_clustered": total_clustered,
"kategori": {
"C0": kategori_dict.get("C0", 0),
"C1": kategori_dict.get("C1", 0),
"C2": kategori_dict.get("C2", 0),
"C3": kategori_dict.get("C3", 0),
},
"silhouette_score": latest_eval.silhouette_score if latest_eval else None,
"model_version": latest_eval.model_version if latest_eval else None,
"tahun_lulus": tahun_lulus # Include tahun for dropdown
}
@router.get("/by-prodi")
def get_stats_by_prodi(
db: Session = Depends(get_db)
):
"""Distribusi alumni dan kategori per prodi"""
# Get alumni count by prodi
prodi_stats = db.query(
Alumni.kode_prodi,
func.count(Alumni.id).label('total')
).group_by(Alumni.kode_prodi).all()
# Get clustering results by prodi and kategori
results = db.query(
Alumni.kode_prodi,
ClusteringResult.kategori,
func.count(ClusteringResult.id).label('count')
).join(
ClusteringResult, Alumni.id == ClusteringResult.alumni_id
).group_by(
Alumni.kode_prodi, ClusteringResult.kategori
).all()
# Format results
prodi_data = {}
for prodi, total in prodi_stats:
prodi_data[prodi] = {"total": total, "C0": 0, "C1": 0, "C2": 0, "C3": 0}
for prodi, kategori, count in results:
if prodi in prodi_data:
prodi_data[prodi][kategori] = count
return prodi_data
@router.get("/by-year")
def get_stats_by_year(
kode_prodi: Optional[str] = Query(None),
db: Session = Depends(get_db)
):
"""Distribusi alumni dan tren kategori C0-C3 per tahun kelulusan secara kronologis (Ascending), mendukung filter prodi"""
prodi_filter = kode_prodi if (kode_prodi and kode_prodi != "all") else None
# 1. Total alumni per tahun lulus urut dari tahun terlama ke terbaru
q_year = db.query(
Alumni.tahun_lulus,
func.count(Alumni.id).label('total')
).filter(Alumni.tahun_lulus.isnot(None))
if prodi_filter:
q_year = q_year.filter(Alumni.kode_prodi == prodi_filter)
year_totals = q_year.group_by(Alumni.tahun_lulus).order_by(Alumni.tahun_lulus.asc()).all()
# 2. Distribusi per kategori C0, C1, C2, C3 per tahun
q_cat = db.query(
Alumni.tahun_lulus,
ClusteringResult.kategori,
func.count(ClusteringResult.id).label('count')
).join(ClusteringResult, Alumni.id == ClusteringResult.alumni_id)\
.filter(Alumni.tahun_lulus.isnot(None))
if prodi_filter:
q_cat = q_cat.filter(Alumni.kode_prodi == prodi_filter)
cat_results = q_cat.group_by(Alumni.tahun_lulus, ClusteringResult.kategori)\
.order_by(Alumni.tahun_lulus.asc()).all()
# Susun struktur data kronologis
years_list = [str(y[0]) for y in year_totals if y[0]]
breakdown_map = {str(y[0]): {"total": y[1], "C0": 0, "C1": 0, "C2": 0, "C3": 0} for y in year_totals if y[0]}
for thn, kat, cnt in cat_results:
thn_str = str(thn)
if thn_str in breakdown_map and kat in breakdown_map[thn_str]:
breakdown_map[thn_str][kat] = cnt
# Format series untuk area / line chart
c0_series = [breakdown_map[y]["C0"] for y in years_list]
c1_series = [breakdown_map[y]["C1"] for y in years_list]
c2_series = [breakdown_map[y]["C2"] for y in years_list]
c3_series = [breakdown_map[y]["C3"] for y in years_list]
return {
"years": years_list,
"breakdown": breakdown_map,
"series": [
{"name": "C0 - Sesuai Profil", "data": c0_series, "color": "#059669"},
{"name": "C1 - Bidang Diketahui", "data": c1_series, "color": "#2563eb"},
{"name": "C2 - Kurang Teridentifikasi", "data": c2_series, "color": "#d97706"},
{"name": "C3 - Belum Bekerja", "data": c3_series, "color": "#64748b"},
],
"totals_by_year": {str(year): total for year, total in year_totals}
}
@router.get("/by-cluster")
def get_stats_by_cluster(
db: Session = Depends(get_db)
):
"""Distribusi alumni per cluster"""
cluster_stats = db.query(
ClusteringResult.cluster,
ClusteringResult.kategori,
func.count(ClusteringResult.id).label('count')
).group_by(
ClusteringResult.cluster,
ClusteringResult.kategori
).all()
# Format results
cluster_data = {}
for cluster, kategori, count in cluster_stats:
key = f"Cluster_{cluster}"
if key not in cluster_data:
cluster_data[key] = {"total": 0, "kategori": {}}
cluster_data[key]["total"] += count
cluster_data[key]["kategori"][kategori] = count
return cluster_data
@router.get("/competency-profile")
def get_competency_profile(
db: Session = Depends(get_db)
):
"""Profil kompetensi rata-rata per kategori"""
# Get average competency scores by kategori
results = db.query(
ClusteringResult.kategori,
func.avg(Kompetensi.etika_lulus).label('etika'),
func.avg(Kompetensi.keahlian_lulus).label('keahlian'),
func.avg(Kompetensi.bahasa_inggris_lulus).label('bahasa_inggris'),
func.avg(Kompetensi.teknologi_informasi_lulus).label('teknologi_informasi'),
func.avg(Kompetensi.komunikasi_lulus).label('komunikasi'),
func.avg(Kompetensi.kerja_sama_lulus).label('kerja_sama'),
func.avg(Kompetensi.pengembangan_diri_lulus).label('pengembangan_diri'),
func.count(Kompetensi.id).label('count')
).join(
Kompetensi, Alumni.id == Kompetensi.alumni_id
).join(
ClusteringResult, Alumni.id == ClusteringResult.alumni_id
).group_by(
ClusteringResult.kategori
).all()
profile = {}
for r in results:
profile[r.kategori] = {
"etika": round(float(r.etika), 2) if r.etika else None,
"keahlian": round(float(r.keahlian), 2) if r.keahlian else None,
"bahasa_inggris": round(float(r.bahasa_inggris), 2) if r.bahasa_inggris else None,
"teknologi_informasi": round(float(r.teknologi_informasi), 2) if r.teknologi_informasi else None,
"komunikasi": round(float(r.komunikasi), 2) if r.komunikasi else None,
"kerja_sama": round(float(r.kerja_sama), 2) if r.kerja_sama else None,
"pengembangan_diri": round(float(r.pengembangan_diri), 2) if r.pengembangan_diri else None,
"count": r.count
}
return profile
@router.get("/company-types")
def get_top_company_types(
limit: int = Query(10, ge=1, le=50),
db: Session = Depends(get_db)
):
"""Top jenis perusahaan berdasarkan hasil clustering"""
results = db.query(
Pekerjaan.jenis_perusahaan,
func.count(Pekerjaan.id).label('count')
).join(
Alumni, Pekerjaan.alumni_id == Alumni.id
).join(
ClusteringResult, Alumni.id == ClusteringResult.alumni_id
).group_by(
Pekerjaan.jenis_perusahaan
).order_by(
func.count(Pekerjaan.id).desc()
).limit(limit).all()
return {
"jenis_perusahaan": [
{"jenis": j, "count": c} for j, c in results
]
}
@router.get("/top-keywords")
def get_top_matched_keywords(
limit: int = Query(10, ge=1, le=50),
db: Session = Depends(get_db)
):
"""Top keywords yang match dengan kamus prodi"""
# Get all matched keywords
results = db.query(ClusteringResult.matched_keyword).filter(
ClusteringResult.matched_keyword.isnot(None),
ClusteringResult.matched_keyword != ""
).all()
# Count keyword frequency
keyword_count = {}
for r in results:
if r.matched_keyword:
# Split by comma for multiple keywords
keywords = [k.strip() for k in r.matched_keyword.split(",")]
for kw in keywords:
keyword_count[kw] = keyword_count.get(kw, 0) + 1
# Sort and limit
sorted_keywords = sorted(keyword_count.items(), key=lambda x: x[1], reverse=True)[:limit]
return {
"keywords": [
{"keyword": kw, "count": c} for kw, c in sorted_keywords
]
}
@router.get("/akreditasi-tables")
def get_akreditasi_tables(
ts_year: Optional[int] = Query(None, description="Tahun Laporan TS (misal 2024 atau 2025)"),
kode_prodi: Optional[str] = Query(None, description="Kode program studi atau 'all'"),
mode: Optional[str] = Query("ts_range", description="'ts_range' (TS-2 s/d TS) atau 'all_years'"),
db: Session = Depends(get_db)
):
"""
Tabel Akreditasi LAM INFOKOM (Kriteria 9 / IKU-1):
- Tabel 2.B.4: Rata-rata Masa Tunggu Lulusan untuk Bekerja Pertama Kali
- Tabel 2.B.5: Kesesuaian Bidang Kerja Lulusan (Infokom vs Non-Infokom & Lingkup Kerja)
Mendukung filter TS-2, TS-1, TS dan pemisahan per Program Studi (TIF, MIF, TKK).
"""
# 1. Dapatkan daftar tahun kelulusan yang valid (< 2026)
year_rows = db.query(Alumni.tahun_lulus)\
.filter(Alumni.tahun_lulus.isnot(None), Alumni.tahun_lulus < 2026)\
.distinct().order_by(Alumni.tahun_lulus.desc()).all()
available_years = [y[0] for y in year_rows if y[0]]
# Tentukan TS Tahun (default 2025 jika tidak disediakan, atau tahun terbaru yang valid)
if not ts_year:
ts_year = 2025 if 2025 in available_years else (available_years[0] if available_years else 2025)
# Tentukan deret tahun yang akan dihitung
if mode == "all_years":
target_years = sorted(available_years)
else:
# Standar LAM INFOKOM: 3 Tahun Lulus Terakhir (TS-2, TS-1, TS)
target_years = [ts_year - 2, ts_year - 1, ts_year]
# Kamus Nama Prodi Resmi JTI Polije
PRODI_INFO = {
"all": "Semua Program Studi (Jurusan TI)",
"55301": "D4 Teknik Informatika (TIF)",
"57401": "D3 Manajemen Informatika (MIF)",
"56401": "D3 Teknik Komputer (TKK)",
"55302": "D4 Teknik Rekayasa Komputer",
"55304": "D3 Manajemen Informatika (Kampus Lain)"
}
prodi_filter = kode_prodi if (kode_prodi and kode_prodi != "all") else None
prodi_label = PRODI_INFO.get(kode_prodi, "Semua Program Studi (Jurusan TI)") if kode_prodi else "Semua Program Studi (Jurusan TI)"
rows_2b4 = []
rows_2b5 = []
sum_lulusan = 0
sum_terlacak = 0
weighted_waktu_sum = 0.0
weighted_waktu_count = 0
sum_infokom = 0
sum_non_infokom = 0
sum_multinasional = 0
sum_nasional = 0
sum_wirausaha = 0
for year in target_years:
# Tentukan label TS (misal TS-2, TS-1, TS)
diff = ts_year - year
if diff == 0:
ts_label = "TS"
elif diff > 0:
ts_label = f"TS-{diff}"
else:
ts_label = f"TS+{abs(diff)}"
display_label = f"{ts_label} ({year})" if mode == "ts_range" else f"{year} ({ts_label})"
# A. Jumlah Lulusan
q_lulus = db.query(Alumni).filter(Alumni.tahun_lulus == year)
if prodi_filter:
q_lulus = q_lulus.filter(Alumni.kode_prodi == prodi_filter)
n_lulus = q_lulus.count()
# B. Jumlah Lulusan yang Terlacak (isi TracerResponse atau memiliki riwayat kerja)
q_terlacak = db.query(Alumni).join(TracerResponse, Alumni.id == TracerResponse.alumni_id)\
.filter(Alumni.tahun_lulus == year)
if prodi_filter:
q_terlacak = q_terlacak.filter(Alumni.kode_prodi == prodi_filter)
n_terlacak = q_terlacak.count()
# C. Rata-rata Waktu Tunggu (Bulan) — dihitung dari alumni yang bekerja dengan bulan_dapat_kerja > 0
q_waktu = db.query(
func.avg(TracerResponse.bulan_dapat_kerja),
func.count(TracerResponse.id)
).join(Alumni, Alumni.id == TracerResponse.alumni_id)\
.filter(
Alumni.tahun_lulus == year,
TracerResponse.bulan_dapat_kerja.isnot(None),
TracerResponse.bulan_dapat_kerja > 0
)
if prodi_filter:
q_waktu = q_waktu.filter(Alumni.kode_prodi == prodi_filter)
avg_waktu_res, count_waktu = q_waktu.first()
avg_waktu = round(float(avg_waktu_res), 2) if avg_waktu_res is not None else 0.0
# D. Profesi Bidang Infokom (C0)
q_c0 = db.query(ClusteringResult).join(Alumni, Alumni.id == ClusteringResult.alumni_id)\
.filter(Alumni.tahun_lulus == year, ClusteringResult.kategori == "C0")
if prodi_filter:
q_c0 = q_c0.filter(Alumni.kode_prodi == prodi_filter)
n_infokom = q_c0.count()
# E. Profesi Bidang NON Infokom (C1 + C2)
q_non_c0 = db.query(ClusteringResult).join(Alumni, Alumni.id == ClusteringResult.alumni_id)\
.filter(Alumni.tahun_lulus == year, ClusteringResult.kategori.in_(["C1", "C2"]))
if prodi_filter:
q_non_c0 = q_non_c0.filter(Alumni.kode_prodi == prodi_filter)
n_non_infokom = q_non_c0.count()
# F. Lingkup Tempat Kerja: Multinasional / Internasional
q_multi = db.query(TracerResponse).join(Alumni, Alumni.id == TracerResponse.alumni_id)\
.filter(
Alumni.tahun_lulus == year,
or_(
TracerResponse.tingkatan_tempat_kerja.ilike("%multinasional%"),
TracerResponse.tingkatan_tempat_kerja.ilike("%internasional%"),
TracerResponse.jenis_perusahaan_full.ilike("%multilateral%")
)
)
if prodi_filter:
q_multi = q_multi.filter(Alumni.kode_prodi == prodi_filter)
n_multi = q_multi.count()
# G. Lingkup Tempat Kerja: Nasional (BUMN, Instansi Pusat, Swasta Nasional)
q_nas = db.query(TracerResponse).join(Alumni, Alumni.id == TracerResponse.alumni_id)\
.filter(
Alumni.tahun_lulus == year,
TracerResponse.tingkatan_tempat_kerja.ilike("%nasional%")
)
if prodi_filter:
q_nas = q_nas.filter(Alumni.kode_prodi == prodi_filter)
n_nas = q_nas.count()
# H. Lingkup Tempat Kerja: Wirausaha
q_wira = db.query(Pekerjaan).join(Alumni, Alumni.id == Pekerjaan.alumni_id)\
.filter(
Alumni.tahun_lulus == year,
or_(
Pekerjaan.status_pekerjaan.ilike("%wiraswasta%"),
Pekerjaan.jenis_perusahaan.ilike("%wiraswasta%")
)
)
if prodi_filter:
q_wira = q_wira.filter(Alumni.kode_prodi == prodi_filter)
n_wira = q_wira.count()
# Masukkan ke baris tabel 2.B.4
rows_2b4.append({
"ts_code": ts_label,
"tahun_lulus": year,
"tahun_label": display_label,
"jumlah_lulusan": n_lulus,
"jumlah_terlacak": n_terlacak,
"persen_terlacak": round((n_terlacak / n_lulus * 100), 1) if n_lulus > 0 else 0,
"rata_waktu_tunggu": avg_waktu
})
# Masukkan ke baris tabel 2.B.5
rows_2b5.append({
"ts_code": ts_label,
"tahun_lulus": year,
"tahun_label": display_label,
"jumlah_lulusan": n_lulus,
"jumlah_terlacak": n_terlacak,
"profesi_infokom": n_infokom,
"profesi_non_infokom": n_non_infokom,
"persen_infokom": round((n_infokom / n_terlacak * 100), 1) if n_terlacak > 0 else 0,
"lingkup_multinasional": n_multi,
"lingkup_nasional": n_nas,
"lingkup_wirausaha": n_wira
})
sum_lulusan += n_lulus
sum_terlacak += n_terlacak
if avg_waktu > 0 and count_waktu:
weighted_waktu_sum += (avg_waktu * count_waktu)
weighted_waktu_count += count_waktu
sum_infokom += n_infokom
sum_non_infokom += n_non_infokom
sum_multinasional += n_multi
sum_nasional += n_nas
sum_wirausaha += n_wira
overall_avg_waktu = round(weighted_waktu_sum / weighted_waktu_count, 2) if weighted_waktu_count > 0 else 0.0
# Summary Totals
total_2b4 = {
"ts_code": "Jumlah",
"tahun_label": "Jumlah / Rata-rata",
"jumlah_lulusan": sum_lulusan,
"jumlah_terlacak": sum_terlacak,
"persen_terlacak": round((sum_terlacak / sum_lulusan * 100), 1) if sum_lulusan > 0 else 0,
"rata_waktu_tunggu": overall_avg_waktu
}
total_2b5 = {
"ts_code": "Jumlah",
"tahun_label": "Jumlah",
"jumlah_lulusan": sum_lulusan,
"jumlah_terlacak": sum_terlacak,
"profesi_infokom": sum_infokom,
"profesi_non_infokom": sum_non_infokom,
"persen_infokom": round((sum_infokom / sum_terlacak * 100), 1) if sum_terlacak > 0 else 0,
"lingkup_multinasional": sum_multinasional,
"lingkup_nasional": sum_nasional,
"lingkup_wirausaha": sum_wirausaha
}
available_prodis = [
{"kode": "all", "nama": "Semua Program Studi (Jurusan TI)"},
{"kode": "55301", "nama": "D4 Teknik Informatika (TIF)"},
{"kode": "57401", "nama": "D3 Manajemen Informatika (MIF)"},
{"kode": "56401", "nama": "D3 Teknik Komputer (TKK)"}
]
return {
"status": "success",
"meta": {
"ts_year": ts_year,
"kode_prodi": kode_prodi or "all",
"prodi_label": prodi_label,
"mode": mode,
"standard": "Instrumen Akreditasi Program Studi (IAPS) LAM INFOKOM Kriteria 9"
},
"available_years": available_years,
"available_prodis": available_prodis,
"tabel_2b4": {
"title": "Tabel 2.B.4 Rata-rata Masa Tunggu Lulusan untuk Bekerja Pertama Kali",
"rows": rows_2b4,
"total": total_2b4
},
"tabel_2b5": {
"title": "Tabel 2.B.5. Kesesuaian Bidang Kerja Lulusan",
"rows": rows_2b5,
"total": total_2b5
}
}