import math from typing import Optional from sqlalchemy.orm import Session from app.services.alumni import AlumniService from app.schemas.alumni import ( AlumniCreate, AlumniUpdate, AlumniResponse, AlumniDetailResponse, ) from app.utils.helpers import format_response class AlumniController: """ Controller handling HTTP requests and response formatting for the Alumni entity. """ @staticmethod def get_alumni(db: Session, alumni_id: int): """Get an alumni record with full details (pekerjaan, kompetensi, tracer responses, clustering).""" alumni = AlumniService.get_by_id_detailed(db, alumni_id) alumni_schema = AlumniDetailResponse.model_validate(alumni) return format_response(True, "Berhasil mengambil data detail alumni", alumni_schema.model_dump()) @staticmethod def get_all_alumni( db: Session, skip: int = 0, limit: int = 10, search: Optional[str] = None, kode_prodi: Optional[str] = None, tahun_lulus: Optional[int] = None, status_kerja: Optional[str] = None, ): """Get filtered alumni list with pagination metadata.""" alumni_list, total = AlumniService.get_filtered( db=db, skip=skip, limit=limit, search=search, kode_prodi=kode_prodi, tahun_lulus=tahun_lulus, status_kerja=status_kerja, ) data = [AlumniResponse.model_validate(a).model_dump() for a in alumni_list] page = (skip // limit) + 1 if limit > 0 else 1 total_pages = math.ceil(total / limit) if limit > 0 else 1 pagination_data = { "items": data, "total": total, "page": page, "limit": limit, "total_pages": total_pages, } return format_response(True, "Berhasil mengambil data alumni", pagination_data) @staticmethod def create_alumni(db: Session, alumni_in: AlumniCreate): """Create a new alumni record and format the response.""" from fastapi import HTTPException try: new_alumni = AlumniService.create(db, alumni_in) alumni_schema = AlumniResponse.model_validate(new_alumni) return format_response(True, "Berhasil menambahkan data alumni", alumni_schema.model_dump()) except HTTPException: raise except Exception as e: db.rollback() raise HTTPException(status_code=400, detail=f"Gagal menambahkan data alumni: {str(e)}") @staticmethod def update_alumni(db: Session, alumni_id: int, alumni_in: AlumniUpdate): """Update an existing alumni record and format the response.""" from fastapi import HTTPException try: updated_alumni = AlumniService.update(db, alumni_id, alumni_in) alumni_schema = AlumniResponse.model_validate(updated_alumni) return format_response(True, "Berhasil memperbarui data alumni", alumni_schema.model_dump()) except HTTPException: raise except Exception as e: db.rollback() raise HTTPException(status_code=400, detail=f"Gagal memperbarui data alumni: {str(e)}") @staticmethod def delete_alumni(db: Session, alumni_id: int): """Delete an alumni record and format the response cleanly with child table cleanup.""" from app.models.models import Pekerjaan, Kompetensi, TracerResponse, ClusteringResult db.query(Pekerjaan).filter(Pekerjaan.alumni_id == alumni_id).delete(synchronize_session=False) db.query(Kompetensi).filter(Kompetensi.alumni_id == alumni_id).delete(synchronize_session=False) db.query(TracerResponse).filter(TracerResponse.alumni_id == alumni_id).delete(synchronize_session=False) db.query(ClusteringResult).filter(ClusteringResult.alumni_id == alumni_id).delete(synchronize_session=False) db.commit() deleted_alumni = AlumniService.delete(db, alumni_id) alumni_schema = AlumniResponse.model_validate(deleted_alumni) return format_response(True, "Berhasil menghapus data alumni", alumni_schema.model_dump()) @staticmethod def public_tracer_lookup(db: Session, lookup_in): """ Public lookup to verify NIM + Gmail for Google Form style response editing. Prevents unauthorized users from viewing or modifying someone else's data. """ from fastapi import HTTPException from app.models.alumni import Alumni from app.models.models import Pekerjaan, TracerResponse nim_clean = lookup_in.nim.strip() alumni = db.query(Alumni).filter(Alumni.nim.ilike(nim_clean)).first() if not alumni: return format_response( True, "NIM belum terdaftar. Silakan isi kuesioner baru.", {"exists": False, "verified": False} ) # Temukan email terdaftar (dari tabel alumni atau tracer_response) registered_email = (alumni.email or "").strip().lower() if not registered_email: tr = db.query(TracerResponse).filter(TracerResponse.alumni_id == alumni.id).first() if tr and tr.email: registered_email = tr.email.strip().lower() # Helper masking email & nama def mask_email(email_str: str) -> str: if not email_str or "@" not in email_str: return "****" user_part, domain_part = email_str.split("@", 1) masked_user = user_part[0] + "***" if len(user_part) <= 2 else user_part[0] + "***" + user_part[-1] return f"{masked_user}@{domain_part}" def mask_name(name_str: str) -> str: if not name_str: return "****" return " ".join([w[0] + "*" * (len(w) - 1) if len(w) > 1 else w + "*" for w in name_str.split()]) # Jika pengguna mengirim email untuk verifikasi if lookup_in.email and lookup_in.email.strip(): input_email = lookup_in.email.strip().lower() if registered_email and input_email != registered_email: raise HTTPException( status_code=403, detail=f"Email ({input_email}) tidak cocok dengan NIM yang terdaftar ({mask_email(registered_email)}). Demi privasi, pengubahan respons hanya dapat dilakukan oleh pemilik email terkait." ) # Jika cocok (atau sebelumnya alumni belum punya email terdaftar): buka akses edit pek = db.query(Pekerjaan).filter(Pekerjaan.alumni_id == alumni.id).first() tr = db.query(TracerResponse).filter(TracerResponse.alumni_id == alumni.id).first() data_extra_val = {} if tr and tr.data_extra: try: import json data_extra_val = json.loads(tr.data_extra) except Exception: data_extra_val = {} alumni_data = { "id": alumni.id, "nim": alumni.nim, "nama": alumni.nama, "kode_prodi": alumni.kode_prodi, "tahun_lulus": alumni.tahun_lulus, "email": registered_email or input_email, "no_hp": alumni.no_hp or (tr.telepon if tr else ""), "status_kerja": alumni.status_kerja or (pek.status_pekerjaan if pek else "bekerja"), "nama_perusahaan": pek.nama_perusahaan if pek else (alumni.instansi or ""), "jabatan": pek.jabatan if pek else (alumni.pekerjaan or ""), "jenis_perusahaan": pek.jenis_perusahaan if pek else (tr.jenis_perusahaan_full if tr else ""), "tingkatan_tempat_kerja": pek.tingkatan_tempat_kerja if pek else "", "hubungan_studi": tr.keeratan_bidang_studi if tr else "", "data_extra": data_extra_val, } return format_response( True, "Verifikasi berhasil. Data respons Anda siap diedit.", {"exists": True, "verified": True, "data": alumni_data} ) # Jika belum memasukkan email (cek status kepemilikan awal) return format_response( True, "NIM ditemukan. Masukkan Email yang terdaftar untuk verifikasi kepemilikan data.", { "exists": True, "verified": False, "has_registered_email": bool(registered_email), "masked_email": mask_email(registered_email) if registered_email else None, "masked_name": mask_name(alumni.nama), "kode_prodi": alumni.kode_prodi, "tahun_lulus": alumni.tahun_lulus, } ) @staticmethod def public_tracer_submit(db: Session, submit_in): """ Public submit handler for alumni tracer study form (Create or Update / Upsert). """ from fastapi import HTTPException from app.models.alumni import Alumni from app.models.models import Pekerjaan, TracerResponse nim_clean = submit_in.nim.strip() alumni = db.query(Alumni).filter(Alumni.nim.ilike(nim_clean)).first() teks_pek = f"{submit_in.status_kerja or ''} {submit_in.jenis_perusahaan or ''} {submit_in.nama_perusahaan or ''} {submit_in.jabatan or ''}".strip() if not teks_pek: teks_pek = "-" if alumni: # Mode UPDATE: Cek kecocokan email jika alumni sudah memiliki email terdaftar if alumni.email and alumni.email.strip(): if alumni.email.strip().lower() != submit_in.email.strip().lower(): raise HTTPException( status_code=403, detail="Email yang Anda masukkan tidak cocok dengan data pendaftaran sebelumnya. Perubahan ditolak." ) # Update data alumni alumni.nama = submit_in.nama.strip() alumni.kode_prodi = submit_in.kode_prodi alumni.tahun_lulus = submit_in.tahun_lulus alumni.email = submit_in.email.strip().lower() alumni.no_hp = submit_in.no_hp.strip() if submit_in.no_hp else alumni.no_hp alumni.status_kerja = submit_in.status_kerja alumni.pekerjaan = submit_in.jabatan alumni.instansi = submit_in.nama_perusahaan # Update / Create Pekerjaan pek = db.query(Pekerjaan).filter(Pekerjaan.alumni_id == alumni.id).first() if not pek: pek = Pekerjaan(alumni_id=alumni.id) db.add(pek) pek.status_pekerjaan = submit_in.status_kerja pek.nama_perusahaan = submit_in.nama_perusahaan pek.jabatan = submit_in.jabatan pek.jenis_perusahaan = submit_in.jenis_perusahaan pek.tingkatan_tempat_kerja = submit_in.tingkatan_tempat_kerja pek.teks_pekerjaan = teks_pek # Update / Create TracerResponse import json tr = db.query(TracerResponse).filter(TracerResponse.alumni_id == alumni.id).first() if not tr: tr = TracerResponse(alumni_id=alumni.id, nim=alumni.nim) db.add(tr) tr.email = submit_in.email.strip().lower() tr.telepon = submit_in.no_hp tr.kode_pt = submit_in.kode_pt or "005019" tr.nik = submit_in.nik tr.npwp = submit_in.npwp tr.status_saat_ini = submit_in.status_kerja tr.bulan_dapat_kerja = submit_in.bulan_dapat_kerja tr.rata_pendapatan = submit_in.rata_pendapatan tr.provinsi_kerja = submit_in.provinsi_kerja tr.kab_kota_kerja = submit_in.kab_kota_kerja tr.nama_perusahaan = submit_in.nama_perusahaan tr.jabatan = submit_in.jabatan tr.jenis_perusahaan_full = submit_in.jenis_perusahaan tr.tingkatan_tempat_kerja = submit_in.tingkatan_tempat_kerja tr.sumber_biaya_studi = submit_in.sumber_biaya_studi tr.pt_studi_lanjut = submit_in.pt_studi_lanjut tr.prodi_studi_lanjut = submit_in.prodi_studi_lanjut tr.sumber_biaya_kuliah = submit_in.sumber_biaya_kuliah tr.keeratan_bidang_studi = submit_in.hubungan_studi tr.tingkat_pendidikan_tepat = submit_in.tingkat_pendidikan_tepat if submit_in.data_extra: tr.data_extra = json.dumps(submit_in.data_extra, ensure_ascii=False) db.commit() db.refresh(alumni) return format_response( True, "Jawaban Anda telah berhasil diperbarui.", {"action": "update", "nim": alumni.nim, "nama": alumni.nama, "email": alumni.email} ) else: # Mode CREATE: Data baru new_alumni = Alumni( nim=nim_clean, nama=submit_in.nama.strip(), kode_prodi=submit_in.kode_prodi, tahun_lulus=submit_in.tahun_lulus, email=submit_in.email.strip().lower(), no_hp=submit_in.no_hp.strip() if submit_in.no_hp else None, status_kerja=submit_in.status_kerja, pekerjaan=submit_in.jabatan, instansi=submit_in.nama_perusahaan, ) db.add(new_alumni) db.flush() # Buat record Pekerjaan pek = Pekerjaan( alumni_id=new_alumni.id, status_pekerjaan=submit_in.status_kerja, nama_perusahaan=submit_in.nama_perusahaan, jabatan=submit_in.jabatan, jenis_perusahaan=submit_in.jenis_perusahaan, tingkatan_tempat_kerja=submit_in.tingkatan_tempat_kerja, provinsi=submit_in.provinsi_kerja, kabupaten_kota=submit_in.kab_kota_kerja, teks_pekerjaan=teks_pek, ) db.add(pek) # Buat record TracerResponse import json tr = TracerResponse( alumni_id=new_alumni.id, nim=new_alumni.nim, email=new_alumni.email, telepon=new_alumni.no_hp, kode_pt=submit_in.kode_pt or "005019", nik=submit_in.nik, npwp=submit_in.npwp, status_saat_ini=submit_in.status_kerja, bulan_dapat_kerja=submit_in.bulan_dapat_kerja, rata_pendapatan=submit_in.rata_pendapatan, provinsi_kerja=submit_in.provinsi_kerja, kab_kota_kerja=submit_in.kab_kota_kerja, nama_perusahaan=submit_in.nama_perusahaan, jabatan=submit_in.jabatan, jenis_perusahaan_full=submit_in.jenis_perusahaan, tingkatan_tempat_kerja=submit_in.tingkatan_tempat_kerja, sumber_biaya_studi=submit_in.sumber_biaya_studi, pt_studi_lanjut=submit_in.pt_studi_lanjut, prodi_studi_lanjut=submit_in.prodi_studi_lanjut, sumber_biaya_kuliah=submit_in.sumber_biaya_kuliah, keeratan_bidang_studi=submit_in.hubungan_studi, tingkat_pendidikan_tepat=submit_in.tingkat_pendidikan_tepat, data_extra=json.dumps(submit_in.data_extra, ensure_ascii=False) if submit_in.data_extra else None, ) db.add(tr) db.commit() db.refresh(new_alumni) return format_response( True, "Jawaban Anda telah direkam. Terima kasih atas partisipasi Anda dalam Tracer Study JTI Polije.", {"action": "create", "nim": new_alumni.nim, "nama": new_alumni.nama, "email": new_alumni.email} )