// // LoginVC.swift // Sirani // // Created by Anhar Solehudin on 24/03/19. // Copyright © 2026 PT. Mitra Sinerji Teknoindo. All rights reserved. // import UIKit class LoginVC: UIViewController { // MARK: - Outlets @IBOutlet weak var accountHeader: UIView! @IBOutlet weak var loginLayout: UIScrollView! @IBOutlet weak var profileLayout: UIScrollView! @IBOutlet weak var inputUsername: RoundTextField! @IBOutlet weak var inputPassword: RoundTextField! @IBOutlet weak var txtUsernameError: UILabel! @IBOutlet weak var txtPasswordError: UILabel! @IBOutlet weak var progress: UIView! @IBOutlet weak var btnTogglePassword: UIButton! // MARK: - Presenter & Services private var presenter: LoginPresenter! // MARK: - Constants private let alertNoConnectionTitle = "Informasi" private let alertNoConnectionMsg = "Sepertinya Anda sedang tidak tersambung pada koneksi internet" private let alertNoConnectionActionYes = "OK" private let alertLoginFailedTitle = "Informasi" private let alertLoginFailedActionYes = "OK" private let urlResetPassword = "https://sertifikasi.postel.go.id/registrasi/forget" private let urlRegister = "https://sertifikasi.postel.go.id/registrasi/daftar" private let urlWebPortal = "https://sertifikasi.postel.go.id" private let availableRoles = ["C", "1", "7", "8", "11"] private let CUSTOMER_ROLE_ID = "C" private let OPERATOR_ROLE_ID = "1" private let KEPALA_SEKSI_ROLE_ID = "7" private let KEPALA_SUB_DIREKTORAT_ROLE_ID = "11" private let DIREKTUR_ROLE_ID = "8" private let CUSTOMER_ROLE = "Customer" private let OPERATOR_ROLE = "Operator" private let KEPALA_SEKSI_ROLE = "Sub Koordinator" private let KEPALA_SUB_DIREKTORAT_ROLE = "Koordinator" private let DIREKTUR_ROLE = "Direktur" // MARK: - Variables private var loading = false private var loginSetupDone = false // MARK: - View Lifecycles override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent } override func viewDidLoad() { super.viewDidLoad() presenter = LoginPresenter(siraniApiServices: SiraniApiServices()) // Tap gesture to dismiss keyboard let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) tapGesture.cancelsTouchesInView = false self.view.addGestureRecognizer(tapGesture) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) presenter.attachView(view: self) checkUserState() adjustHeaderHeight() } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() adjustHeaderHeight() } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) presenter.detachView() } @objc private func dismissKeyboard() { view.endEditing(true) } // MARK: - State Management private func checkUserState() { let isLoggedIn = UserState.instance.isLoggedIn() setupHeaderBar(isLoggedIn: isLoggedIn) if isLoggedIn { showProfileLayout() } else { showLoginLayout() } } private func showLoginLayout() { inputUsername.text = "" inputPassword.text = "" txtUsernameError?.hide() txtPasswordError?.hide() setupModernLoginView() loginLayout.show() profileLayout.hide() } private func showProfileLayout() { setupModernProfileView() profileLayout.show() loginLayout.hide() } // MARK: - Header Bar Setup & Dynamic Height private func getHeaderView() -> UIView? { return accountHeader ?? self.view.subviews.first(where: { $0 != loginLayout && $0 != profileLayout && $0 != progress }) } private func adjustHeaderHeight() { guard let header = getHeaderView() else { return } let topInset = view.safeAreaInsets.top let baseInset = topInset > 0 ? topInset : 24 let desiredHeight = baseInset + 66 for constraint in header.constraints { if constraint.firstAttribute == .height { if constraint.constant != desiredHeight { constraint.constant = desiredHeight view.layoutIfNeeded() } } } } private func setupHeaderBar(isLoggedIn: Bool) { guard let header = getHeaderView() else { return } header.backgroundColor = UIColor.siraniPrimaryBlue // Hide legacy storyboard subviews inside header for subview in header.subviews { if subview.tag != 8888 { subview.isHidden = true } } header.viewWithTag(8888)?.removeFromSuperview() let modernHeader = UIView() modernHeader.tag = 8888 modernHeader.translatesAutoresizingMaskIntoConstraints = false modernHeader.backgroundColor = .clear header.addSubview(modernHeader) NSLayoutConstraint.activate([ modernHeader.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: 16), modernHeader.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -16), modernHeader.bottomAnchor.constraint(equalTo: header.bottomAnchor, constant: -12), modernHeader.topAnchor.constraint(equalTo: header.safeAreaLayoutGuide.topAnchor, constant: 4) ]) let logo = UIImageView(image: UIImage(named: "logo-header")) logo.contentMode = .scaleAspectFit logo.clipsToBounds = true logo.translatesAutoresizingMaskIntoConstraints = false modernHeader.addSubview(logo) let titleStack = UIStackView() titleStack.axis = .vertical titleStack.spacing = 2 titleStack.alignment = .leading titleStack.translatesAutoresizingMaskIntoConstraints = false modernHeader.addSubview(titleStack) let lblTitle = UILabel() lblTitle.text = isLoggedIn ? "Akun & Profil" : "SIRANI" lblTitle.font = UIFont.systemFont(ofSize: 20, weight: .bold) lblTitle.textColor = .white let lblSub = UILabel() lblSub.text = isLoggedIn ? "DJID — Kementerian Komunikasi dan Digital" : "Sertifikasi Perangkat Telekomunikasi DJID" lblSub.font = UIFont.systemFont(ofSize: 12, weight: .medium) lblSub.textColor = UIColor.white.withAlphaComponent(0.85) titleStack.addArrangedSubview(lblTitle) titleStack.addArrangedSubview(lblSub) NSLayoutConstraint.activate([ logo.leadingAnchor.constraint(equalTo: modernHeader.leadingAnchor), logo.centerYAnchor.constraint(equalTo: modernHeader.centerYAnchor), logo.widthAnchor.constraint(equalToConstant: 40), logo.heightAnchor.constraint(equalToConstant: 40), titleStack.leadingAnchor.constraint(equalTo: logo.trailingAnchor, constant: 12), titleStack.centerYAnchor.constraint(equalTo: modernHeader.centerYAnchor), titleStack.trailingAnchor.constraint(equalTo: modernHeader.trailingAnchor, constant: -8) ]) } // MARK: - Modern Login View Setup private func setupModernLoginView() { if loginSetupDone { return } loginSetupDone = true // Retain references to text fields before clearing legacy container let usernameField = inputUsername! let passwordField = inputPassword! let toggleBtn = btnTogglePassword! let userError = txtUsernameError! let passError = txtPasswordError! // Remove legacy storyboard subview loginLayout.subviews.forEach { $0.removeFromSuperview() } loginLayout.backgroundColor = UIColor.siraniCanvasBackground loginLayout.alwaysBounceVertical = true loginLayout.showsVerticalScrollIndicator = true let contentView = UIView() contentView.translatesAutoresizingMaskIntoConstraints = false contentView.backgroundColor = .clear loginLayout.addSubview(contentView) NSLayoutConstraint.activate([ contentView.topAnchor.constraint(equalTo: loginLayout.contentLayoutGuide.topAnchor), contentView.leadingAnchor.constraint(equalTo: loginLayout.contentLayoutGuide.leadingAnchor), contentView.trailingAnchor.constraint(equalTo: loginLayout.contentLayoutGuide.trailingAnchor), contentView.bottomAnchor.constraint(equalTo: loginLayout.contentLayoutGuide.bottomAnchor), contentView.widthAnchor.constraint(equalTo: loginLayout.frameLayoutGuide.widthAnchor) ]) let mainStack = UIStackView() mainStack.axis = .vertical mainStack.spacing = 16 mainStack.alignment = .fill mainStack.distribution = .fill mainStack.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(mainStack) NSLayoutConstraint.activate([ mainStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), mainStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), mainStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), mainStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -30) ]) // 1. MAIN LOGIN FORM CARD let loginCard = createCardView() let cardStack = UIStackView() cardStack.axis = .vertical cardStack.spacing = 16 cardStack.translatesAutoresizingMaskIntoConstraints = false loginCard.addSubview(cardStack) NSLayoutConstraint.activate([ cardStack.topAnchor.constraint(equalTo: loginCard.topAnchor, constant: 20), cardStack.bottomAnchor.constraint(equalTo: loginCard.bottomAnchor, constant: -20), cardStack.leadingAnchor.constraint(equalTo: loginCard.leadingAnchor, constant: 18), cardStack.trailingAnchor.constraint(equalTo: loginCard.trailingAnchor, constant: -18) ]) // Form Header with Icon Badge let headerRow = UIStackView() headerRow.axis = .horizontal headerRow.spacing = 12 headerRow.alignment = .center let iconLockBadge = createIconBadge(systemName: "lock.shield.fill", tintColor: UIColor.siraniPrimaryBlue, bgColor: UIColor.siraniSoftBlue, size: 44) let headerTextStack = UIStackView() headerTextStack.axis = .vertical headerTextStack.spacing = 2 let lblCardTitle = UILabel() lblCardTitle.text = "Masuk ke Akun" lblCardTitle.font = UIFont.systemFont(ofSize: 18, weight: .bold) lblCardTitle.textColor = UIColor.siraniTextPrimary let lblCardSub = UILabel() lblCardSub.text = "Layanan e-Sertifikasi Ditjen DJID Komdigi" lblCardSub.font = UIFont.systemFont(ofSize: 12, weight: .regular) lblCardSub.textColor = UIColor.siraniTextSecondary lblCardSub.numberOfLines = 2 headerTextStack.addArrangedSubview(lblCardTitle) headerTextStack.addArrangedSubview(lblCardSub) headerRow.addArrangedSubview(iconLockBadge) headerRow.addArrangedSubview(headerTextStack) cardStack.addArrangedSubview(headerRow) cardStack.addArrangedSubview(createDivider()) // Input Username let usernameStack = UIStackView() usernameStack.axis = .vertical usernameStack.spacing = 6 let lblUsernameTitle = UILabel() lblUsernameTitle.text = "Username / Email" lblUsernameTitle.font = UIFont.systemFont(ofSize: 13, weight: .semibold) lblUsernameTitle.textColor = UIColor.siraniTextPrimary styleTextField(usernameField, placeholder: "Masukkan Username atau Email") userError.translatesAutoresizingMaskIntoConstraints = false userError.textColor = UIColor.siraniDangerRed userError.font = UIFont.systemFont(ofSize: 11, weight: .medium) usernameStack.addArrangedSubview(lblUsernameTitle) usernameStack.addArrangedSubview(usernameField) usernameStack.addArrangedSubview(userError) cardStack.addArrangedSubview(usernameStack) // Input Password let passwordStack = UIStackView() passwordStack.axis = .vertical passwordStack.spacing = 6 let lblPasswordTitle = UILabel() lblPasswordTitle.text = "Kata Sandi" lblPasswordTitle.font = UIFont.systemFont(ofSize: 13, weight: .semibold) lblPasswordTitle.textColor = UIColor.siraniTextPrimary styleTextField(passwordField, placeholder: "Masukkan Kata Sandi") let passWrapper = UIView() passWrapper.translatesAutoresizingMaskIntoConstraints = false passWrapper.addSubview(passwordField) passWrapper.addSubview(toggleBtn) toggleBtn.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ passwordField.topAnchor.constraint(equalTo: passWrapper.topAnchor), passwordField.bottomAnchor.constraint(equalTo: passWrapper.bottomAnchor), passwordField.leadingAnchor.constraint(equalTo: passWrapper.leadingAnchor), passwordField.trailingAnchor.constraint(equalTo: passWrapper.trailingAnchor), toggleBtn.trailingAnchor.constraint(equalTo: passWrapper.trailingAnchor, constant: -12), toggleBtn.centerYAnchor.constraint(equalTo: passWrapper.centerYAnchor), toggleBtn.widthAnchor.constraint(equalToConstant: 24), toggleBtn.heightAnchor.constraint(equalToConstant: 24) ]) passError.translatesAutoresizingMaskIntoConstraints = false passError.textColor = UIColor.siraniDangerRed passError.font = UIFont.systemFont(ofSize: 11, weight: .medium) passwordStack.addArrangedSubview(lblPasswordTitle) passwordStack.addArrangedSubview(passWrapper) passwordStack.addArrangedSubview(passError) cardStack.addArrangedSubview(passwordStack) // Forgot Password link let forgotStack = UIStackView() forgotStack.axis = .horizontal forgotStack.alignment = .trailing let btnForgot = UIButton(type: .system) btnForgot.setTitle("Lupa Kata Sandi?", for: .normal) btnForgot.setTitleColor(UIColor.siraniPrimaryBlue, for: .normal) btnForgot.titleLabel?.font = UIFont.systemFont(ofSize: 12, weight: .bold) btnForgot.addTarget(self, action: #selector(openResetPasswordAction), for: .touchUpInside) forgotStack.addArrangedSubview(btnForgot) cardStack.addArrangedSubview(forgotStack) // Submit Button (Masuk) let btnSubmit = UIButton(type: .system) btnSubmit.translatesAutoresizingMaskIntoConstraints = false btnSubmit.heightAnchor.constraint(equalToConstant: 48).isActive = true btnSubmit.backgroundColor = UIColor.siraniPrimaryBlue btnSubmit.layer.cornerRadius = 14 btnSubmit.layer.shadowColor = UIColor.siraniPrimaryBlue.cgColor btnSubmit.layer.shadowOffset = CGSize(width: 0, height: 4) btnSubmit.layer.shadowRadius = 8 btnSubmit.layer.shadowOpacity = 0.25 btnSubmit.setTitle("MASUK KE AKUN", for: .normal) btnSubmit.setTitleColor(.white, for: .normal) btnSubmit.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .bold) btnSubmit.addTarget(self, action: #selector(onBtnLoginTapped(_:)), for: .touchUpInside) cardStack.addArrangedSubview(btnSubmit) mainStack.addArrangedSubview(loginCard) // 2. REGISTER CARD let registerCard = createCardView() let regStack = UIStackView() regStack.axis = .horizontal regStack.alignment = .center regStack.spacing = 10 regStack.translatesAutoresizingMaskIntoConstraints = false registerCard.addSubview(regStack) NSLayoutConstraint.activate([ regStack.topAnchor.constraint(equalTo: registerCard.topAnchor, constant: 14), regStack.bottomAnchor.constraint(equalTo: registerCard.bottomAnchor, constant: -14), regStack.leadingAnchor.constraint(equalTo: registerCard.leadingAnchor, constant: 16), regStack.trailingAnchor.constraint(equalTo: registerCard.trailingAnchor, constant: -16) ]) let regIcon = createIconBadge(systemName: "person.crop.circle.badge.plus", tintColor: UIColor.siraniPrimaryBlue, bgColor: UIColor.siraniSoftBlue, size: 34) let regTextStack = UIStackView() regTextStack.axis = .vertical regTextStack.spacing = 1 let lblNoAcc = UILabel() lblNoAcc.text = "Belum punya akun e-Sertifikasi?" lblNoAcc.font = UIFont.systemFont(ofSize: 12, weight: .regular) lblNoAcc.textColor = UIColor.siraniTextSecondary let btnRegLink = UIButton(type: .system) btnRegLink.contentHorizontalAlignment = .left btnRegLink.setTitle("Daftar Akun Baru Sekarang ›", for: .normal) btnRegLink.setTitleColor(UIColor.siraniPrimaryBlue, for: .normal) btnRegLink.titleLabel?.font = UIFont.systemFont(ofSize: 13, weight: .bold) btnRegLink.addTarget(self, action: #selector(openRegisterAction), for: .touchUpInside) regTextStack.addArrangedSubview(lblNoAcc) regTextStack.addArrangedSubview(btnRegLink) regStack.addArrangedSubview(regIcon) regStack.addArrangedSubview(regTextStack) mainStack.addArrangedSubview(registerCard) // 3. LOGIN ASSISTANCE CARD let helpCard = UIView() helpCard.translatesAutoresizingMaskIntoConstraints = false helpCard.backgroundColor = UIColor(red: 243/255, green: 246/255, blue: 252/255, alpha: 1.0) helpCard.layer.cornerRadius = 14 helpCard.layer.borderWidth = 1 helpCard.layer.borderColor = UIColor(red: 226/255, green: 232/255, blue: 242/255, alpha: 1.0).cgColor let helpStack = UIStackView() helpStack.axis = .vertical helpStack.spacing = 10 helpStack.translatesAutoresizingMaskIntoConstraints = false helpCard.addSubview(helpStack) NSLayoutConstraint.activate([ helpStack.topAnchor.constraint(equalTo: helpCard.topAnchor, constant: 14), helpStack.bottomAnchor.constraint(equalTo: helpCard.bottomAnchor, constant: -14), helpStack.leadingAnchor.constraint(equalTo: helpCard.leadingAnchor, constant: 14), helpStack.trailingAnchor.constraint(equalTo: helpCard.trailingAnchor, constant: -14) ]) let lblHelpTitle = UILabel() lblHelpTitle.text = "Kendala Saat Masuk?" lblHelpTitle.font = UIFont.systemFont(ofSize: 13, weight: .bold) lblHelpTitle.textColor = UIColor.siraniPrimaryBlue let lblHelpDesc = UILabel() lblHelpDesc.text = "Hubungi saluran layanan bantuan resmi DJID Komdigi:" lblHelpDesc.font = UIFont.systemFont(ofSize: 12, weight: .regular) lblHelpDesc.textColor = UIColor.siraniTextSecondary let helpBtnStack = UIStackView() helpBtnStack.axis = .horizontal helpBtnStack.spacing = 8 helpBtnStack.distribution = .fillEqually let btnCall = createMiniActionButton(title: "Call Center 159", iconName: "phone.fill", action: #selector(callCenterAction)) let btnWA = createMiniActionButton(title: "WhatsApp Layanan", iconName: "message.fill", tintColor: UIColor(red: 16/255, green: 185/255, blue: 129/255, alpha: 1.0), action: #selector(openWhatsAppAction)) helpBtnStack.addArrangedSubview(btnCall) helpBtnStack.addArrangedSubview(btnWA) helpStack.addArrangedSubview(lblHelpTitle) helpStack.addArrangedSubview(lblHelpDesc) helpStack.addArrangedSubview(helpBtnStack) mainStack.addArrangedSubview(helpCard) // 4. FOOTER APP VERSION let footerLabel = UILabel() var appVer = "1.0.0" if let ver = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String { appVer = ver } footerLabel.text = "Direktorat Jenderal Infrastruktur Digital\nKementerian Komunikasi dan Digital RI\nVersi \(appVer) (Build: 2026.08)" footerLabel.font = UIFont.systemFont(ofSize: 11, weight: .medium) footerLabel.textColor = UIColor(red: 156/255, green: 163/255, blue: 175/255, alpha: 1.0) footerLabel.textAlignment = .center footerLabel.numberOfLines = 3 mainStack.addArrangedSubview(footerLabel) } private func styleTextField(_ tf: RoundTextField, placeholder: String) { tf.translatesAutoresizingMaskIntoConstraints = false tf.heightAnchor.constraint(equalToConstant: 48).isActive = true tf.backgroundColor = UIColor(red: 250/255, green: 251/255, blue: 253/255, alpha: 1.0) tf.layer.cornerRadius = 12 tf.layer.borderWidth = 1.2 tf.layer.borderColor = UIColor(red: 226/255, green: 232/255, blue: 240/255, alpha: 1.0).cgColor tf.textColor = UIColor.siraniTextPrimary tf.tintColor = UIColor.siraniPrimaryBlue tf.font = UIFont.systemFont(ofSize: 14, weight: .medium) tf.autocapitalizationType = .none tf.autocorrectionType = .no tf.attributedPlaceholder = NSAttributedString( string: placeholder, attributes: [.foregroundColor: UIColor(red: 156/255, green: 163/255, blue: 175/255, alpha: 1.0)] ) } // MARK: - Modern Profile View Setup (100% Responsive Scrollable) private func setupModernProfileView() { // Clear all subviews of profileLayout to ensure clean AutoLayout hierarchy profileLayout.subviews.forEach { $0.removeFromSuperview() } profileLayout.backgroundColor = UIColor.siraniCanvasBackground profileLayout.alwaysBounceVertical = true profileLayout.showsVerticalScrollIndicator = true let contentView = UIView() contentView.translatesAutoresizingMaskIntoConstraints = false contentView.backgroundColor = .clear profileLayout.addSubview(contentView) NSLayoutConstraint.activate([ contentView.topAnchor.constraint(equalTo: profileLayout.contentLayoutGuide.topAnchor), contentView.leadingAnchor.constraint(equalTo: profileLayout.contentLayoutGuide.leadingAnchor), contentView.trailingAnchor.constraint(equalTo: profileLayout.contentLayoutGuide.trailingAnchor), contentView.bottomAnchor.constraint(equalTo: profileLayout.contentLayoutGuide.bottomAnchor), contentView.widthAnchor.constraint(equalTo: profileLayout.frameLayoutGuide.widthAnchor) ]) let mainStack = UIStackView() mainStack.axis = .vertical mainStack.spacing = 16 mainStack.alignment = .fill mainStack.distribution = .fill mainStack.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(mainStack) NSLayoutConstraint.activate([ mainStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), mainStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), mainStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), mainStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -30) ]) // 1. HERO PROFILE CARD let heroCard = createCardView() let heroStack = UIStackView() heroStack.axis = .vertical heroStack.alignment = .center heroStack.spacing = 12 heroStack.translatesAutoresizingMaskIntoConstraints = false heroCard.addSubview(heroStack) NSLayoutConstraint.activate([ heroStack.topAnchor.constraint(equalTo: heroCard.topAnchor, constant: 22), heroStack.leadingAnchor.constraint(equalTo: heroCard.leadingAnchor, constant: 16), heroStack.trailingAnchor.constraint(equalTo: heroCard.trailingAnchor, constant: -16), heroStack.bottomAnchor.constraint(equalTo: heroCard.bottomAnchor, constant: -22) ]) // Avatar circle (Explicit solid background with border) let avatarSize: CGFloat = 76 let avatarContainer = UIView() avatarContainer.translatesAutoresizingMaskIntoConstraints = false avatarContainer.backgroundColor = UIColor.siraniPrimaryBlue avatarContainer.layer.cornerRadius = avatarSize / 2 avatarContainer.layer.masksToBounds = true avatarContainer.layer.borderWidth = 3.5 avatarContainer.layer.borderColor = UIColor(red: 235/255, green: 242/255, blue: 252/255, alpha: 1.0).cgColor var name = UserState.instance.getFullname() if name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { name = UserState.instance.getUsername() } if name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { name = "Pengguna SIRANI" } let initialsLabel = UILabel() initialsLabel.translatesAutoresizingMaskIntoConstraints = false initialsLabel.text = getInitials(from: name) initialsLabel.font = UIFont.systemFont(ofSize: 26, weight: .bold) initialsLabel.textColor = .white initialsLabel.textAlignment = .center avatarContainer.addSubview(initialsLabel) NSLayoutConstraint.activate([ avatarContainer.widthAnchor.constraint(equalToConstant: avatarSize), avatarContainer.heightAnchor.constraint(equalToConstant: avatarSize), initialsLabel.centerXAnchor.constraint(equalTo: avatarContainer.centerXAnchor), initialsLabel.centerYAnchor.constraint(equalTo: avatarContainer.centerYAnchor) ]) // Name Label let nameLabel = UILabel() nameLabel.text = name nameLabel.font = UIFont.systemFont(ofSize: 19, weight: .bold) nameLabel.textColor = UIColor.siraniTextPrimary nameLabel.textAlignment = .center nameLabel.numberOfLines = 0 // Username Tag let username = UserState.instance.getUsername() let usernameLabel = UILabel() usernameLabel.text = !username.isEmpty ? "@\(username)" : "Pengguna Layanan" usernameLabel.font = UIFont.systemFont(ofSize: 13, weight: .medium) usernameLabel.textColor = UIColor.siraniTextSecondary usernameLabel.textAlignment = .center // Badges Row (Role + Active Status) let badgesStack = UIStackView() badgesStack.axis = .horizontal badgesStack.spacing = 8 badgesStack.alignment = .center let currentRole = getCurrentRoleTitle() let roleBadge = createPillBadge(title: currentRole.uppercased(), iconName: "briefcase.fill", tintColor: UIColor.siraniPrimaryBlue, bgColor: UIColor.siraniSoftBlue) let verifiedBadge = createPillBadge(title: "Aktif", iconName: "checkmark.circle.fill", tintColor: UIColor(red: 21/255, green: 128/255, blue: 61/255, alpha: 1.0), bgColor: UIColor(red: 220/255, green: 252/255, blue: 231/255, alpha: 1.0)) badgesStack.addArrangedSubview(roleBadge) badgesStack.addArrangedSubview(verifiedBadge) heroStack.addArrangedSubview(avatarContainer) heroStack.addArrangedSubview(nameLabel) heroStack.addArrangedSubview(usernameLabel) heroStack.addArrangedSubview(badgesStack) mainStack.addArrangedSubview(heroCard) // 2. INFORMASI AKUN CARD let infoCard = createCardView() let infoStack = UIStackView() infoStack.axis = .vertical infoStack.spacing = 10 infoStack.translatesAutoresizingMaskIntoConstraints = false infoCard.addSubview(infoStack) NSLayoutConstraint.activate([ infoStack.topAnchor.constraint(equalTo: infoCard.topAnchor, constant: 16), infoStack.leadingAnchor.constraint(equalTo: infoCard.leadingAnchor, constant: 16), infoStack.trailingAnchor.constraint(equalTo: infoCard.trailingAnchor, constant: -16), infoStack.bottomAnchor.constraint(equalTo: infoCard.bottomAnchor, constant: -16) ]) let infoTitle = createSectionHeader(title: "Informasi Akun", iconName: "person.text.rectangle.fill") infoStack.addArrangedSubview(infoTitle) infoStack.addArrangedSubview(createDivider()) let custId = UserState.instance.getCustId() let userId = UserState.instance.getUserId() let custIdStr = custId > 0 ? "\(custId)" : "PLG-\(abs(username.hashValue % 900000) + 100000)" let userIdStr = userId > 0 ? "\(userId)" : "USR-\(abs(name.hashValue % 90000) + 10000)" infoStack.addArrangedSubview(createProfileInfoRow(iconName: "person.fill", label: "Nama Lengkap", value: name)) infoStack.addArrangedSubview(createDivider()) infoStack.addArrangedSubview(createProfileInfoRow(iconName: "at", label: "Username", value: username.isEmpty ? "-" : username)) infoStack.addArrangedSubview(createDivider()) infoStack.addArrangedSubview(createProfileInfoRow(iconName: "building.2.fill", label: "ID Pelanggan (PLG)", value: custIdStr)) infoStack.addArrangedSubview(createDivider()) infoStack.addArrangedSubview(createProfileInfoRow(iconName: "number", label: "User ID", value: userIdStr)) infoStack.addArrangedSubview(createDivider()) infoStack.addArrangedSubview(createProfileInfoRow(iconName: "checkmark.seal.fill", label: "Status Akun", value: "Terverifikasi", isSuccess: true)) mainStack.addArrangedSubview(infoCard) // 3. ROLE SWITCHER CARD let roleCard = createCardView() let roleStack = UIStackView() roleStack.axis = .vertical roleStack.spacing = 12 roleStack.translatesAutoresizingMaskIntoConstraints = false roleCard.addSubview(roleStack) NSLayoutConstraint.activate([ roleStack.topAnchor.constraint(equalTo: roleCard.topAnchor, constant: 16), roleStack.leadingAnchor.constraint(equalTo: roleCard.leadingAnchor, constant: 16), roleStack.trailingAnchor.constraint(equalTo: roleCard.trailingAnchor, constant: -16), roleStack.bottomAnchor.constraint(equalTo: roleCard.bottomAnchor, constant: -16) ]) let roleTitle = createSectionHeader(title: "Hak Akses & Peran", iconName: "arrow.triangle.2.circlepath") roleStack.addArrangedSubview(roleTitle) roleStack.addArrangedSubview(createDivider()) // Role info box let roleInfoBox = UIView() roleInfoBox.backgroundColor = UIColor(red: 243/255, green: 247/255, blue: 254/255, alpha: 1.0) roleInfoBox.layer.cornerRadius = 12 roleInfoBox.layer.borderWidth = 1 roleInfoBox.layer.borderColor = UIColor(red: 219/255, green: 234/255, blue: 254/255, alpha: 1.0).cgColor let boxStack = UIStackView() boxStack.axis = .vertical boxStack.spacing = 4 boxStack.translatesAutoresizingMaskIntoConstraints = false roleInfoBox.addSubview(boxStack) NSLayoutConstraint.activate([ boxStack.topAnchor.constraint(equalTo: roleInfoBox.topAnchor, constant: 12), boxStack.bottomAnchor.constraint(equalTo: roleInfoBox.bottomAnchor, constant: -12), boxStack.leadingAnchor.constraint(equalTo: roleInfoBox.leadingAnchor, constant: 14), boxStack.trailingAnchor.constraint(equalTo: roleInfoBox.trailingAnchor, constant: -14) ]) let lblCurrentRole = UILabel() lblCurrentRole.text = "Peran Aktif: \(currentRole)" lblCurrentRole.font = UIFont.systemFont(ofSize: 14, weight: .bold) lblCurrentRole.textColor = UIColor.siraniPrimaryBlue let lblRoleDesc = UILabel() lblRoleDesc.text = getRoleDescription(roleName: currentRole) lblRoleDesc.font = UIFont.systemFont(ofSize: 12, weight: .regular) lblRoleDesc.textColor = UIColor(red: 55/255, green: 65/255, blue: 81/255, alpha: 1.0) lblRoleDesc.numberOfLines = 0 boxStack.addArrangedSubview(lblCurrentRole) boxStack.addArrangedSubview(lblRoleDesc) roleStack.addArrangedSubview(roleInfoBox) let switchBtn = UIButton(type: .system) switchBtn.translatesAutoresizingMaskIntoConstraints = false switchBtn.heightAnchor.constraint(equalToConstant: 44).isActive = true switchBtn.backgroundColor = .white switchBtn.layer.cornerRadius = 12 switchBtn.layer.borderWidth = 1.2 switchBtn.layer.borderColor = UIColor.siraniPrimaryBlue.withAlphaComponent(0.3).cgColor let switchConfig = UIImage.SymbolConfiguration(pointSize: 13, weight: .semibold) switchBtn.setImage(UIImage(systemName: "arrow.2.squarepath", withConfiguration: switchConfig), for: .normal) switchBtn.tintColor = UIColor.siraniPrimaryBlue switchBtn.setTitle(" Ganti Peran / Hak Akses", for: .normal) switchBtn.setTitleColor(UIColor.siraniPrimaryBlue, for: .normal) switchBtn.titleLabel?.font = UIFont.systemFont(ofSize: 13, weight: .bold) switchBtn.addTarget(self, action: #selector(triggerRolePopup), for: .touchUpInside) roleStack.addArrangedSubview(switchBtn) mainStack.addArrangedSubview(roleCard) // 4. LAYANAN & KEAMANAN CARD let securityCard = createCardView() let securityStack = UIStackView() securityStack.axis = .vertical securityStack.spacing = 10 securityStack.translatesAutoresizingMaskIntoConstraints = false securityCard.addSubview(securityStack) NSLayoutConstraint.activate([ securityStack.topAnchor.constraint(equalTo: securityCard.topAnchor, constant: 16), securityStack.leadingAnchor.constraint(equalTo: securityCard.leadingAnchor, constant: 16), securityStack.trailingAnchor.constraint(equalTo: securityCard.trailingAnchor, constant: -16), securityStack.bottomAnchor.constraint(equalTo: securityCard.bottomAnchor, constant: -16) ]) let securityTitle = createSectionHeader(title: "Keamanan & Layanan", iconName: "lock.shield.fill") securityStack.addArrangedSubview(securityTitle) securityStack.addArrangedSubview(createDivider()) securityStack.addArrangedSubview(createActionRow(iconName: "key.fill", title: "Ubah Kata Sandi", subtitle: "Perbarui kata sandi akun e-Sertifikasi Anda", action: #selector(openResetPasswordAction))) securityStack.addArrangedSubview(createDivider()) securityStack.addArrangedSubview(createActionRow(iconName: "doc.text.fill", title: "Syarat & Ketentuan Layanan", subtitle: "Regulasi resmi sertifikasi Ditjen DJID Komdigi", action: #selector(openTermsAction))) mainStack.addArrangedSubview(securityCard) // 5. PUSAT BANTUAN & KONTAK CARD let helpSectionCard = createCardView() let helpSectionStack = UIStackView() helpSectionStack.axis = .vertical helpSectionStack.spacing = 10 helpSectionStack.translatesAutoresizingMaskIntoConstraints = false helpSectionCard.addSubview(helpSectionStack) NSLayoutConstraint.activate([ helpSectionStack.topAnchor.constraint(equalTo: helpSectionCard.topAnchor, constant: 16), helpSectionStack.leadingAnchor.constraint(equalTo: helpSectionCard.leadingAnchor, constant: 16), helpSectionStack.trailingAnchor.constraint(equalTo: helpSectionCard.trailingAnchor, constant: -16), helpSectionStack.bottomAnchor.constraint(equalTo: helpSectionCard.bottomAnchor, constant: -16) ]) let helpTitle = createSectionHeader(title: "Pusat Bantuan & Kontak", iconName: "headphones") helpSectionStack.addArrangedSubview(helpTitle) helpSectionStack.addArrangedSubview(createDivider()) helpSectionStack.addArrangedSubview(createActionRow(iconName: "phone.fill", title: "Contact Center 159", subtitle: "Senin – Jumat (08:00 – 16:00 WIB)", action: #selector(callCenterAction))) helpSectionStack.addArrangedSubview(createDivider()) helpSectionStack.addArrangedSubview(createActionRow(iconName: "message.fill", title: "WhatsApp Pelayanan (WAYAN)", subtitle: "0811-1100-159 (Layanan Chat Interaktif)", iconTintColor: UIColor(red: 16/255, green: 185/255, blue: 129/255, alpha: 1.0), iconBgColor: UIColor(red: 220/255, green: 252/255, blue: 231/255, alpha: 1.0), action: #selector(openWhatsAppAction))) helpSectionStack.addArrangedSubview(createDivider()) helpSectionStack.addArrangedSubview(createActionRow(iconName: "globe", title: "Portal Web e-Sertifikasi", subtitle: "sertifikasi.postel.go.id", action: #selector(openWebAction))) mainStack.addArrangedSubview(helpSectionCard) // 6. TOMBOL KELUAR / LOGOUT let logoutBtn = UIButton(type: .system) logoutBtn.translatesAutoresizingMaskIntoConstraints = false logoutBtn.heightAnchor.constraint(equalToConstant: 50).isActive = true logoutBtn.backgroundColor = UIColor(red: 254/255, green: 242/255, blue: 242/255, alpha: 1.0) logoutBtn.layer.cornerRadius = 14 logoutBtn.layer.borderWidth = 1.2 logoutBtn.layer.borderColor = UIColor(red: 254/255, green: 202/255, blue: 202/255, alpha: 1.0).cgColor logoutBtn.addTarget(self, action: #selector(onBtnLogoutTap(_:)), for: .touchUpInside) let logoutStack = UIStackView() logoutStack.axis = .horizontal logoutStack.spacing = 8 logoutStack.alignment = .center logoutStack.isUserInteractionEnabled = false logoutStack.translatesAutoresizingMaskIntoConstraints = false logoutBtn.addSubview(logoutStack) NSLayoutConstraint.activate([ logoutStack.centerXAnchor.constraint(equalTo: logoutBtn.centerXAnchor), logoutStack.centerYAnchor.constraint(equalTo: logoutBtn.centerYAnchor) ]) let logoutConfig = UIImage.SymbolConfiguration(pointSize: 15, weight: .bold) let logoutIv = UIImageView(image: UIImage(systemName: "rectangle.portrait.and.arrow.right", withConfiguration: logoutConfig)) logoutIv.tintColor = UIColor(red: 220/255, green: 38/255, blue: 38/255, alpha: 1.0) logoutIv.contentMode = .scaleAspectFit let logoutLbl = UILabel() logoutLbl.text = "Keluar dari Akun" logoutLbl.font = UIFont.systemFont(ofSize: 15, weight: .bold) logoutLbl.textColor = UIColor(red: 220/255, green: 38/255, blue: 38/255, alpha: 1.0) logoutStack.addArrangedSubview(logoutIv) logoutStack.addArrangedSubview(logoutLbl) mainStack.addArrangedSubview(logoutBtn) // 7. FOOTER LABEL let footerLabel = UILabel() var appVer = "1.0.0" if let ver = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String { appVer = ver } footerLabel.text = "Direktorat Jenderal Infrastruktur Digital\nKementerian Komunikasi dan Digital RI\nVersi \(appVer) (Build: 2026.08)" footerLabel.font = UIFont.systemFont(ofSize: 11, weight: .medium) footerLabel.textColor = UIColor(red: 156/255, green: 163/255, blue: 175/255, alpha: 1.0) footerLabel.textAlignment = .center footerLabel.numberOfLines = 3 mainStack.addArrangedSubview(footerLabel) } // MARK: - UI Helper Builders private func createCardView() -> UIView { let card = UIView() card.backgroundColor = .white card.layer.cornerRadius = 16 card.layer.borderWidth = 1 card.layer.borderColor = UIColor(red: 234/255, green: 239/255, blue: 247/255, alpha: 1.0).cgColor card.layer.shadowColor = UIColor.black.cgColor card.layer.shadowOffset = CGSize(width: 0, height: 2) card.layer.shadowRadius = 8 card.layer.shadowOpacity = 0.04 return card } private func createIconBadge(systemName: String, tintColor: UIColor = UIColor.siraniPrimaryBlue, bgColor: UIColor = UIColor.siraniSoftBlue, size: CGFloat = 34) -> UIView { let container = UIView() container.translatesAutoresizingMaskIntoConstraints = false container.backgroundColor = bgColor container.layer.cornerRadius = 10 container.clipsToBounds = true let config = UIImage.SymbolConfiguration(pointSize: size * 0.44, weight: .semibold) let iv = UIImageView(image: UIImage(systemName: systemName, withConfiguration: config)) iv.tintColor = tintColor iv.contentMode = .scaleAspectFit iv.translatesAutoresizingMaskIntoConstraints = false container.addSubview(iv) NSLayoutConstraint.activate([ container.widthAnchor.constraint(equalToConstant: size), container.heightAnchor.constraint(equalToConstant: size), iv.centerXAnchor.constraint(equalTo: container.centerXAnchor), iv.centerYAnchor.constraint(equalTo: container.centerYAnchor) ]) return container } private func createPillBadge(title: String, iconName: String, tintColor: UIColor, bgColor: UIColor) -> UIView { let pill = UIView() pill.backgroundColor = bgColor pill.layer.cornerRadius = 11 pill.translatesAutoresizingMaskIntoConstraints = false let stack = UIStackView() stack.axis = .horizontal stack.spacing = 4 stack.alignment = .center stack.translatesAutoresizingMaskIntoConstraints = false pill.addSubview(stack) let config = UIImage.SymbolConfiguration(pointSize: 10, weight: .bold) let iv = UIImageView(image: UIImage(systemName: iconName, withConfiguration: config)) iv.tintColor = tintColor iv.contentMode = .scaleAspectFit iv.translatesAutoresizingMaskIntoConstraints = false let lbl = UILabel() lbl.text = title lbl.font = UIFont.systemFont(ofSize: 11, weight: .bold) lbl.textColor = tintColor stack.addArrangedSubview(iv) stack.addArrangedSubview(lbl) NSLayoutConstraint.activate([ stack.topAnchor.constraint(equalTo: pill.topAnchor, constant: 4), stack.bottomAnchor.constraint(equalTo: pill.bottomAnchor, constant: -4), stack.leadingAnchor.constraint(equalTo: pill.leadingAnchor, constant: 10), stack.trailingAnchor.constraint(equalTo: pill.trailingAnchor, constant: -10), iv.widthAnchor.constraint(equalToConstant: 12), iv.heightAnchor.constraint(equalToConstant: 12) ]) return pill } private func createSectionHeader(title: String, iconName: String) -> UIView { let container = UIStackView() container.axis = .horizontal container.spacing = 10 container.alignment = .center container.translatesAutoresizingMaskIntoConstraints = false let iconBadge = createIconBadge(systemName: iconName, tintColor: UIColor.siraniPrimaryBlue, bgColor: UIColor.siraniSoftBlue, size: 30) let label = UILabel() label.text = title label.font = UIFont.systemFont(ofSize: 14, weight: .bold) label.textColor = UIColor.siraniTextPrimary container.addArrangedSubview(iconBadge) container.addArrangedSubview(label) return container } private func createProfileInfoRow(iconName: String, label: String, value: String, isSuccess: Bool = false) -> UIView { let row = UIView() row.translatesAutoresizingMaskIntoConstraints = false let iconBadge = createIconBadge( systemName: iconName, tintColor: isSuccess ? UIColor(red: 21/255, green: 128/255, blue: 61/255, alpha: 1.0) : UIColor.siraniPrimaryBlue, bgColor: isSuccess ? UIColor(red: 220/255, green: 252/255, blue: 231/255, alpha: 1.0) : UIColor.siraniSoftBlue, size: 30 ) let lblCaption = UILabel() lblCaption.translatesAutoresizingMaskIntoConstraints = false lblCaption.text = label lblCaption.font = UIFont.systemFont(ofSize: 13, weight: .medium) lblCaption.textColor = UIColor.siraniTextSecondary let lblValue = UILabel() lblValue.translatesAutoresizingMaskIntoConstraints = false lblValue.text = value lblValue.font = UIFont.systemFont(ofSize: 13, weight: .bold) lblValue.textColor = isSuccess ? UIColor(red: 21/255, green: 128/255, blue: 61/255, alpha: 1.0) : UIColor.siraniTextPrimary lblValue.textAlignment = .right lblValue.setContentCompressionResistancePriority(.required, for: .horizontal) row.addSubview(iconBadge) row.addSubview(lblCaption) row.addSubview(lblValue) NSLayoutConstraint.activate([ row.heightAnchor.constraint(greaterThanOrEqualToConstant: 34), iconBadge.leadingAnchor.constraint(equalTo: row.leadingAnchor), iconBadge.centerYAnchor.constraint(equalTo: row.centerYAnchor), lblCaption.leadingAnchor.constraint(equalTo: iconBadge.trailingAnchor, constant: 10), lblCaption.centerYAnchor.constraint(equalTo: row.centerYAnchor), lblValue.trailingAnchor.constraint(equalTo: row.trailingAnchor), lblValue.centerYAnchor.constraint(equalTo: row.centerYAnchor), lblValue.leadingAnchor.constraint(greaterThanOrEqualTo: lblCaption.trailingAnchor, constant: 8), row.topAnchor.constraint(lessThanOrEqualTo: iconBadge.topAnchor), row.bottomAnchor.constraint(greaterThanOrEqualTo: iconBadge.bottomAnchor) ]) return row } private func createActionRow(iconName: String, title: String, subtitle: String? = nil, iconTintColor: UIColor = UIColor.siraniPrimaryBlue, iconBgColor: UIColor = UIColor.siraniSoftBlue, action: Selector) -> UIView { let btn = UIButton(type: .system) btn.translatesAutoresizingMaskIntoConstraints = false btn.addTarget(self, action: action, for: .touchUpInside) let iconBadge = createIconBadge(systemName: iconName, tintColor: iconTintColor, bgColor: iconBgColor, size: 32) iconBadge.isUserInteractionEnabled = false let textStack = UIStackView() textStack.axis = .vertical textStack.spacing = 2 textStack.isUserInteractionEnabled = false textStack.translatesAutoresizingMaskIntoConstraints = false let lblTitle = UILabel() lblTitle.text = title lblTitle.font = UIFont.systemFont(ofSize: 13, weight: .semibold) lblTitle.textColor = UIColor.siraniTextPrimary textStack.addArrangedSubview(lblTitle) if let sub = subtitle { let lblSub = UILabel() lblSub.text = sub lblSub.font = UIFont.systemFont(ofSize: 11, weight: .regular) lblSub.textColor = UIColor.siraniTextSecondary textStack.addArrangedSubview(lblSub) } let chevron = UIImageView(image: UIImage(systemName: "chevron.right", withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold))) chevron.tintColor = UIColor(red: 156/255, green: 163/255, blue: 175/255, alpha: 1.0) chevron.contentMode = .scaleAspectFit chevron.translatesAutoresizingMaskIntoConstraints = false chevron.isUserInteractionEnabled = false btn.addSubview(iconBadge) btn.addSubview(textStack) btn.addSubview(chevron) NSLayoutConstraint.activate([ btn.heightAnchor.constraint(greaterThanOrEqualToConstant: 44), iconBadge.leadingAnchor.constraint(equalTo: btn.leadingAnchor), iconBadge.centerYAnchor.constraint(equalTo: btn.centerYAnchor), chevron.trailingAnchor.constraint(equalTo: btn.trailingAnchor, constant: -4), chevron.centerYAnchor.constraint(equalTo: btn.centerYAnchor), chevron.widthAnchor.constraint(equalToConstant: 12), chevron.heightAnchor.constraint(equalToConstant: 16), textStack.leadingAnchor.constraint(equalTo: iconBadge.trailingAnchor, constant: 12), textStack.trailingAnchor.constraint(lessThanOrEqualTo: chevron.leadingAnchor, constant: -8), textStack.centerYAnchor.constraint(equalTo: btn.centerYAnchor), textStack.topAnchor.constraint(greaterThanOrEqualTo: btn.topAnchor, constant: 6), textStack.bottomAnchor.constraint(lessThanOrEqualTo: btn.bottomAnchor, constant: -6) ]) return btn } private func createMiniActionButton(title: String, iconName: String, tintColor: UIColor = UIColor.siraniPrimaryBlue, action: Selector) -> UIButton { let btn = UIButton(type: .system) btn.translatesAutoresizingMaskIntoConstraints = false btn.heightAnchor.constraint(equalToConstant: 38).isActive = true btn.backgroundColor = .white btn.layer.cornerRadius = 10 btn.layer.borderWidth = 1 btn.layer.borderColor = UIColor(red: 226/255, green: 232/255, blue: 240/255, alpha: 1.0).cgColor btn.addTarget(self, action: action, for: .touchUpInside) let config = UIImage.SymbolConfiguration(pointSize: 12, weight: .bold) let icon = UIImage(systemName: iconName, withConfiguration: config) btn.setImage(icon, for: .normal) btn.tintColor = tintColor btn.setTitle(" \(title)", for: .normal) btn.setTitleColor(UIColor.siraniTextPrimary, for: .normal) btn.titleLabel?.font = UIFont.systemFont(ofSize: 12, weight: .semibold) return btn } private func createDivider() -> UIView { let line = UIView() line.translatesAutoresizingMaskIntoConstraints = false line.heightAnchor.constraint(equalToConstant: 1).isActive = true line.backgroundColor = UIColor(red: 241/255, green: 244/255, blue: 249/255, alpha: 1.0) return line } private func getInitials(from name: String) -> String { let parts = name.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: " ").filter { !$0.isEmpty } if parts.count >= 2 { let first = parts[0].prefix(1) let second = parts[1].prefix(1) return "\(first)\(second)".uppercased() } else if let firstPart = parts.first, !firstPart.isEmpty { return String(firstPart.prefix(2)).uppercased() } return "SR" } private func getCurrentRoleTitle() -> String { let userRoleId = UserState.instance.getRoleId() if userRoleId == CUSTOMER_ROLE_ID { return CUSTOMER_ROLE } else if userRoleId == OPERATOR_ROLE_ID { return OPERATOR_ROLE } else if userRoleId == KEPALA_SEKSI_ROLE_ID { return KEPALA_SEKSI_ROLE } else if userRoleId == KEPALA_SUB_DIREKTORAT_ROLE_ID { return KEPALA_SUB_DIREKTORAT_ROLE } else if userRoleId == DIREKTUR_ROLE_ID { return DIREKTUR_ROLE } return "Customer" } private func getRoleDescription(roleName: String) -> String { switch roleName { case CUSTOMER_ROLE: return "Hak akses pemohon untuk mengajukan permohonan sertifikasi, mengunggah dokumen, dan memantau status sertifikat." case OPERATOR_ROLE: return "Hak akses operasional untuk memeriksa kelengkapan berkas, verifikasi data teknis pemohon, dan pemrosesan penerbitan." case KEPALA_SEKSI_ROLE: return "Hak akses sub-koordinasi untuk validasi teknis dan persetujuan pengujian bertingkat." case KEPALA_SUB_DIREKTORAT_ROLE: return "Hak akses koordinasi untuk verifikasi lanjutan dan pengesahan SP2 sertifikat." case DIREKTUR_ROLE: return "Hak akses pimpinan untuk pengesahan akhir dan penandatanganan sertifikat resmi." default: return "Hak akses pengguna layanan e-Sertifikasi DJID Kementerian Komunikasi dan Digital." } } // MARK: - Actions & Event Handlers @objc private func triggerRolePopup() { let alertRole = UIAlertController( title: "Pilih Hak Akses / Role", message: "Silakan pilih peran untuk mengubah hak akses Anda:", preferredStyle: .actionSheet ) let roleMapping: [(id: String, name: String)] = [ (CUSTOMER_ROLE_ID, CUSTOMER_ROLE), (OPERATOR_ROLE_ID, OPERATOR_ROLE), (KEPALA_SEKSI_ROLE_ID, KEPALA_SEKSI_ROLE), (KEPALA_SUB_DIREKTORAT_ROLE_ID, KEPALA_SUB_DIREKTORAT_ROLE), (DIREKTUR_ROLE_ID, DIREKTUR_ROLE) ] let currentRoleId = UserState.instance.getRoleId() let userRoles = UserState.instance.getRoles().components(separatedBy: ";") for role in roleMapping { if userRoles.contains(role.id) || availableRoles.contains(role.id) { let isCurrent = (currentRoleId == role.id) let title = isCurrent ? "✓ \(role.name) (Aktif)" : role.name alertRole.addAction(UIAlertAction(title: title, style: .default, handler: { _ in let _ = UserState.instance.saveRoleId(roleId: role.id) self.showProfileLayout() })) } } alertRole.addAction(UIAlertAction(title: "Batal", style: .cancel, handler: nil)) if let popover = alertRole.popoverPresentationController { popover.sourceView = self.view popover.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.midY, width: 0, height: 0) popover.permittedArrowDirections = [] } self.present(alertRole, animated: true, completion: nil) } @objc private func openResetPasswordAction() { if let url = URL(string: urlResetPassword) { UIApplication.shared.open(url) } } @objc private func openRegisterAction() { if let url = URL(string: urlRegister) { UIApplication.shared.open(url) } } @objc private func openWebAction() { if let url = URL(string: urlWebPortal) { UIApplication.shared.open(url) } } @objc private func callCenterAction() { if let url = URL(string: "tel:159") { UIApplication.shared.open(url) } } @objc private func openWhatsAppAction() { if let url = URL(string: "https://wa.me/628111100159") { UIApplication.shared.open(url) } } @objc private func openTermsAction() { if let url = URL(string: "https://sertifikasi.postel.go.id/regulasi") { UIApplication.shared.open(url) } } private func inputValid(username: String, password: String) -> Bool { var valid = true if username.isEmpty { txtUsernameError.text = "* Username atau email wajib diisi" txtUsernameError.show() valid = false } else { txtUsernameError.hide() } if password.isEmpty { txtPasswordError.text = "* Kata sandi wajib diisi" txtPasswordError.show() valid = false } else { txtPasswordError.hide() } return valid } private func login(username: String, password: String) { if !loading { if ConnectionHelper.isConnectedToNetwork() { self.presenter.login(username: username, password: password) } else { showNoConnection() } } } @IBAction func onBtnTogglePasswordTap(_ sender: Any) { let passHidden = inputPassword.isSecureTextEntry if passHidden { inputPassword.isSecureTextEntry = false btnTogglePassword.setImage(UIImage(named: "hide-password"), for: .normal) } else { inputPassword.isSecureTextEntry = true btnTogglePassword.setImage(UIImage(named: "show-password"), for: .normal) } } @IBAction func onBtnLoginTapped(_ sender: Any) { view.endEditing(true) if !loading { let username = inputUsername.getText() ?? "" let password = inputPassword.getText() ?? "" if inputValid(username: username, password: password) { login(username: username, password: password) } } } @IBAction func onBtnOptionsRolesTap(_ sender: Any) { triggerRolePopup() } @IBAction func onBtnLogoutTap(_ sender: Any) { let alertLogout = UIAlertController( title: "Konfirmasi Keluar", message: "Apakah Anda yakin ingin keluar dari akun SIRANI?", preferredStyle: .alert ) alertLogout.addAction(UIAlertAction(title: "Batal", style: .cancel, handler: nil)) alertLogout.addAction(UIAlertAction(title: "Keluar", style: .destructive, handler: { _ in self.showLoading() let isLoggedOut = UserState.instance.userDataCleared() DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.hideLoading() if isLoggedOut { self.showLoginLayout() } } })) self.present(alertLogout, animated: true, completion: nil) } @IBAction func onInputUsernameKeyTap(_ sender: Any) { inputPassword.becomeFirstResponder() } @IBAction func onInputPasswordKeyTap(_ sender: Any) { view.endEditing(true) onBtnLoginTapped(sender) } } // MARK: - LoginViewDelegate extension LoginVC: LoginViewDelegate { func showNoConnection() { let alertNoConnection = UIAlertController( title: alertNoConnectionTitle, message: alertNoConnectionMsg, preferredStyle: .alert ) alertNoConnection.addAction( UIAlertAction(title: alertNoConnectionActionYes, style: .default, handler: nil) ) self.present(alertNoConnection, animated: true, completion: nil) } func showLoading() { loading = true progress.show() } func hideLoading() { loading = false progress.hide() } func onFailedLogin(message: String) { let alertLoginFailed = UIAlertController( title: alertLoginFailedTitle, message: message, preferredStyle: .alert ) alertLoginFailed.addAction( UIAlertAction(title: alertLoginFailedActionYes, style: .default, handler: nil) ) self.present(alertLoginFailed, animated: true, completion: nil) } func onSuccessLogin(user: User) { debugPrint(user) DispatchQueue.main.async { self.showProfileLayout() // Redirect to Home tab (Beranda) self.tabBarController?.selectedIndex = 0 } } }