// // DetailAplikasiVC.swift // Sirani // // Created by Antigravity on 08/09/26. // Copyright © 2026 PT. Mitra Sinerji Teknoindo. All rights reserved. // import UIKit class DetailAplikasiVC: UIViewController { // MARK: - Data var aplikasiData: Aplikasi? override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent } // MARK: - UI Components private let topStatusBarBackground = UIView() private let toolbarView = UIView() private let btnBack = UIButton(type: .custom) private let titleLabel = UILabel() private let subtitleLabel = UILabel() private let btnShare = UIButton(type: .custom) private let scrollView = UIScrollView() private let contentView = UIView() private let contentStack = UIStackView() // Toast Alert private let toastView = UIView() private let toastLabel = UILabel() // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() setupUI() populateData() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) AppUtility.lockOrientation(.portrait) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) AppUtility.lockOrientation(.all) } // MARK: - UI Setup private func setupUI() { view.backgroundColor = UIColor(red: 247/255, green: 249/255, blue: 252/255, alpha: 1.0) // 1. Top Status Bar & Toolbar topStatusBarBackground.translatesAutoresizingMaskIntoConstraints = false topStatusBarBackground.backgroundColor = UIColor.siraniPrimaryBlue view.addSubview(topStatusBarBackground) toolbarView.translatesAutoresizingMaskIntoConstraints = false toolbarView.backgroundColor = UIColor.siraniPrimaryBlue view.addSubview(toolbarView) // Back Button btnBack.translatesAutoresizingMaskIntoConstraints = false if let backImg = UIImage(named: "toolbar-back") { btnBack.setImage(backImg, for: .normal) } else { btnBack.setImage(UIImage(systemName: "chevron.backward"), for: .normal) btnBack.tintColor = .white } btnBack.addTarget(self, action: #selector(onBackTap), for: .touchUpInside) toolbarView.addSubview(btnBack) // Titles Stack let titleStack = UIStackView() titleStack.translatesAutoresizingMaskIntoConstraints = false titleStack.axis = .vertical titleStack.spacing = 2 titleStack.alignment = .leading toolbarView.addSubview(titleStack) titleLabel.font = UIFont.systemFont(ofSize: 17, weight: .bold) titleLabel.textColor = .white titleLabel.text = "Detail Aplikasi" titleStack.addArrangedSubview(titleLabel) subtitleLabel.font = UIFont.systemFont(ofSize: 11, weight: .medium) subtitleLabel.textColor = UIColor.white.withAlphaComponent(0.8) subtitleLabel.text = "Permohonan Layanan DJID" titleStack.addArrangedSubview(subtitleLabel) // Share Button btnShare.translatesAutoresizingMaskIntoConstraints = false btnShare.setImage(UIImage(systemName: "square.and.arrow.up"), for: .normal) btnShare.tintColor = .white btnShare.addTarget(self, action: #selector(onShareTap), for: .touchUpInside) toolbarView.addSubview(btnShare) // 2. ScrollView & ContentView scrollView.translatesAutoresizingMaskIntoConstraints = false scrollView.showsVerticalScrollIndicator = true scrollView.alwaysBounceVertical = true view.addSubview(scrollView) contentView.translatesAutoresizingMaskIntoConstraints = false scrollView.addSubview(contentView) contentStack.translatesAutoresizingMaskIntoConstraints = false contentStack.axis = .vertical contentStack.spacing = 16 contentStack.alignment = .fill contentStack.distribution = .fill contentView.addSubview(contentStack) // 3. Toast Feedback View toastView.translatesAutoresizingMaskIntoConstraints = false toastView.backgroundColor = UIColor(red: 26/255, green: 29/255, blue: 38/255, alpha: 0.9) toastView.layer.cornerRadius = 10 toastView.layer.masksToBounds = true toastView.alpha = 0 view.addSubview(toastView) toastLabel.translatesAutoresizingMaskIntoConstraints = false toastLabel.font = UIFont.systemFont(ofSize: 12, weight: .semibold) toastLabel.textColor = .white toastLabel.textAlignment = .center toastView.addSubview(toastLabel) // Layout Constraints NSLayoutConstraint.activate([ // Status Bar Background topStatusBarBackground.topAnchor.constraint(equalTo: view.topAnchor), topStatusBarBackground.leadingAnchor.constraint(equalTo: view.leadingAnchor), topStatusBarBackground.trailingAnchor.constraint(equalTo: view.trailingAnchor), topStatusBarBackground.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), // Toolbar toolbarView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), toolbarView.leadingAnchor.constraint(equalTo: view.leadingAnchor), toolbarView.trailingAnchor.constraint(equalTo: view.trailingAnchor), toolbarView.heightAnchor.constraint(equalToConstant: 56), btnBack.leadingAnchor.constraint(equalTo: toolbarView.leadingAnchor, constant: 16), btnBack.centerYAnchor.constraint(equalTo: toolbarView.centerYAnchor), btnBack.widthAnchor.constraint(equalToConstant: 24), btnBack.heightAnchor.constraint(equalToConstant: 24), titleStack.leadingAnchor.constraint(equalTo: btnBack.trailingAnchor, constant: 16), titleStack.trailingAnchor.constraint(equalTo: btnShare.leadingAnchor, constant: -12), titleStack.centerYAnchor.constraint(equalTo: toolbarView.centerYAnchor), btnShare.trailingAnchor.constraint(equalTo: toolbarView.trailingAnchor, constant: -16), btnShare.centerYAnchor.constraint(equalTo: toolbarView.centerYAnchor), btnShare.widthAnchor.constraint(equalToConstant: 28), btnShare.heightAnchor.constraint(equalToConstant: 28), // ScrollView scrollView.topAnchor.constraint(equalTo: toolbarView.bottomAnchor), scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), contentView.topAnchor.constraint(equalTo: scrollView.topAnchor), contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), contentStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), contentStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), contentStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), contentStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -24), // Toast toastView.centerXAnchor.constraint(equalTo: view.centerXAnchor), toastView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20), toastLabel.topAnchor.constraint(equalTo: toastView.topAnchor, constant: 10), toastLabel.bottomAnchor.constraint(equalTo: toastView.bottomAnchor, constant: -10), toastLabel.leadingAnchor.constraint(equalTo: toastView.leadingAnchor, constant: 16), toastLabel.trailingAnchor.constraint(equalTo: toastView.trailingAnchor, constant: -16) ]) } // MARK: - Populate Data private func populateData() { guard let data = aplikasiData else { return } subtitleLabel.text = "APL ID: \(data.apl_id)" // 1. Hero Card: Status & Identitas Aplikasi contentStack.addArrangedSubview(createHeroCard(data: data)) // 2. Data Pemohon (Perusahaan) contentStack.addArrangedSubview(createPemohonCard(data: data)) // 3. Data Perangkat contentStack.addArrangedSubview(createPerangkatCard(data: data)) // 4. Disposisi & Status Proses contentStack.addArrangedSubview(createDisposisiCard(data: data)) // 5. Close Button let btnClose = UIButton(type: .system) btnClose.translatesAutoresizingMaskIntoConstraints = false btnClose.setTitle("Kembali ke Daftar Aplikasi", for: .normal) btnClose.setTitleColor(.white, for: .normal) btnClose.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .bold) btnClose.backgroundColor = UIColor.siraniPrimaryBlue btnClose.layer.cornerRadius = 14 btnClose.layer.shadowColor = UIColor.siraniPrimaryBlue.cgColor btnClose.layer.shadowOffset = CGSize(width: 0, height: 4) btnClose.layer.shadowRadius = 8 btnClose.layer.shadowOpacity = 0.25 btnClose.heightAnchor.constraint(equalToConstant: 48).isActive = true btnClose.addTarget(self, action: #selector(onBackTap), for: .touchUpInside) contentStack.addArrangedSubview(btnClose) } // MARK: - Card Builders // 1. Hero Card (APL ID, ID Izin, Status, Resi) private func createHeroCard(data: Aplikasi) -> UIView { let card = createCardContainer() let stack = UIStackView() stack.translatesAutoresizingMaskIntoConstraints = false stack.axis = .vertical stack.spacing = 14 card.addSubview(stack) // Header Row: Badge Icon + APL ID + Status Pill let headerRow = UIStackView() headerRow.axis = .horizontal headerRow.alignment = .center headerRow.spacing = 10 let iconBadge = UIView() iconBadge.translatesAutoresizingMaskIntoConstraints = false iconBadge.backgroundColor = UIColor.siraniSoftBlue iconBadge.layer.cornerRadius = 10 iconBadge.layer.masksToBounds = true let icon = UIImageView(image: UIImage(systemName: "app.badge.fill")) icon.tintColor = UIColor.siraniPrimaryBlue icon.contentMode = .scaleAspectFit icon.translatesAutoresizingMaskIntoConstraints = false iconBadge.addSubview(icon) NSLayoutConstraint.activate([ iconBadge.widthAnchor.constraint(equalToConstant: 38), iconBadge.heightAnchor.constraint(equalToConstant: 38), icon.centerXAnchor.constraint(equalTo: iconBadge.centerXAnchor), icon.centerYAnchor.constraint(equalTo: iconBadge.centerYAnchor), icon.widthAnchor.constraint(equalToConstant: 20), icon.heightAnchor.constraint(equalToConstant: 20) ]) headerRow.addArrangedSubview(iconBadge) let idStack = UIStackView() idStack.axis = .vertical idStack.spacing = 2 let aplTitle = UILabel() aplTitle.font = UIFont.systemFont(ofSize: 10, weight: .bold) aplTitle.textColor = UIColor(red: 140/255, green: 155/255, blue: 175/255, alpha: 1.0) aplTitle.text = "APL ID" idStack.addArrangedSubview(aplTitle) let aplVal = UILabel() aplVal.font = UIFont.systemFont(ofSize: 18, weight: .bold) aplVal.textColor = UIColor.siraniPrimaryBlue aplVal.text = "\(data.apl_id)" idStack.addArrangedSubview(aplVal) headerRow.addArrangedSubview(idStack) let spacer = UIView() spacer.setContentHuggingPriority(.defaultLow, for: .horizontal) headerRow.addArrangedSubview(spacer) // Status Pill let statusPill = UIView() statusPill.translatesAutoresizingMaskIntoConstraints = false statusPill.layer.cornerRadius = 8 statusPill.layer.masksToBounds = true let statusStr = data.StatusStr.trimmingCharacters(in: .whitespacesAndNewlines) let statusColorHex = data.StatusBgColor.trimmingCharacters(in: .whitespacesAndNewlines) let statusLabel = UILabel() statusLabel.translatesAutoresizingMaskIntoConstraints = false statusLabel.font = UIFont.systemFont(ofSize: 11, weight: .bold) statusLabel.text = statusStr.isEmpty ? "PERMOHONAN" : statusStr if let customColor = UIColor(hexString: statusColorHex), !statusColorHex.isEmpty { statusPill.backgroundColor = customColor.withAlphaComponent(0.15) statusLabel.textColor = customColor } else { statusPill.backgroundColor = UIColor(red: 230/255, green: 244/255, blue: 234/255, alpha: 1.0) statusLabel.textColor = UIColor(red: 19/255, green: 115/255, blue: 51/255, alpha: 1.0) } statusPill.addSubview(statusLabel) NSLayoutConstraint.activate([ statusLabel.topAnchor.constraint(equalTo: statusPill.topAnchor, constant: 6), statusLabel.bottomAnchor.constraint(equalTo: statusPill.bottomAnchor, constant: -6), statusLabel.leadingAnchor.constraint(equalTo: statusPill.leadingAnchor, constant: 10), statusLabel.trailingAnchor.constraint(equalTo: statusPill.trailingAnchor, constant: -10) ]) headerRow.addArrangedSubview(statusPill) stack.addArrangedSubview(headerRow) stack.addArrangedSubview(createDivider()) // ID Izin Box with Copy Action let idIzinBox = UIView() idIzinBox.backgroundColor = UIColor(red: 248/255, green: 250/255, blue: 254/255, alpha: 1.0) idIzinBox.layer.cornerRadius = 10 idIzinBox.layer.borderWidth = 1 idIzinBox.layer.borderColor = UIColor(red: 230/255, green: 237/255, blue: 248/255, alpha: 1.0).cgColor let idIzinStack = UIStackView() idIzinStack.translatesAutoresizingMaskIntoConstraints = false idIzinStack.axis = .horizontal idIzinStack.alignment = .center idIzinStack.spacing = 10 idIzinBox.addSubview(idIzinStack) let idIzinInner = UIStackView() idIzinInner.axis = .vertical idIzinInner.spacing = 2 let idIzinTitle = UILabel() idIzinTitle.font = UIFont.systemFont(ofSize: 10, weight: .bold) idIzinTitle.textColor = UIColor(red: 140/255, green: 155/255, blue: 175/255, alpha: 1.0) idIzinTitle.text = "ID IZIN PERMOHONAN" idIzinInner.addArrangedSubview(idIzinTitle) let idIzinVal = UILabel() idIzinVal.font = UIFont.systemFont(ofSize: 13, weight: .bold) idIzinVal.textColor = UIColor(red: 30/255, green: 40/255, blue: 60/255, alpha: 1.0) idIzinVal.text = data.id_izin.isEmpty ? "-" : data.id_izin idIzinVal.numberOfLines = 1 idIzinInner.addArrangedSubview(idIzinVal) idIzinStack.addArrangedSubview(idIzinInner) let copySpacer = UIView() copySpacer.setContentHuggingPriority(.defaultLow, for: .horizontal) idIzinStack.addArrangedSubview(copySpacer) // Copy Button let btnCopy = UIButton(type: .system) btnCopy.setImage(UIImage(systemName: "doc.on.doc"), for: .normal) btnCopy.setTitle(" Salin", for: .normal) btnCopy.titleLabel?.font = UIFont.systemFont(ofSize: 12, weight: .bold) btnCopy.tintColor = UIColor.siraniPrimaryBlue btnCopy.backgroundColor = UIColor.white btnCopy.layer.cornerRadius = 8 btnCopy.layer.borderWidth = 1 btnCopy.layer.borderColor = UIColor(red: 220/255, green: 230/255, blue: 245/255, alpha: 1.0).cgColor btnCopy.contentEdgeInsets = UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10) btnCopy.addTarget(self, action: #selector(onCopyIdIzin), for: .touchUpInside) idIzinStack.addArrangedSubview(btnCopy) NSLayoutConstraint.activate([ idIzinStack.topAnchor.constraint(equalTo: idIzinBox.topAnchor, constant: 10), idIzinStack.bottomAnchor.constraint(equalTo: idIzinBox.bottomAnchor, constant: -10), idIzinStack.leadingAnchor.constraint(equalTo: idIzinBox.leadingAnchor, constant: 12), idIzinStack.trailingAnchor.constraint(equalTo: idIzinBox.trailingAnchor, constant: -12) ]) stack.addArrangedSubview(idIzinBox) // 2-Column Resi Box let resiStack = UIStackView() resiStack.axis = .horizontal resiStack.distribution = .fillEqually resiStack.spacing = 10 let resiNoBox = createMiniInfoBox(title: "NO. RESI", value: data.resi_no.isEmpty ? "-" : data.resi_no) let resiDateFormatted = formatDateString(data.resi_date) let resiDateBox = createMiniInfoBox(title: "TANGGAL RESI", value: resiDateFormatted) resiStack.addArrangedSubview(resiNoBox) resiStack.addArrangedSubview(resiDateBox) stack.addArrangedSubview(resiStack) NSLayoutConstraint.activate([ stack.topAnchor.constraint(equalTo: card.topAnchor, constant: 16), stack.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -16), stack.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16), stack.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16) ]) return card } // 2. Data Pemohon Card private func createPemohonCard(data: Aplikasi) -> UIView { let card = createCardContainer() let stack = UIStackView() stack.translatesAutoresizingMaskIntoConstraints = false stack.axis = .vertical stack.spacing = 12 card.addSubview(stack) // Section Header stack.addArrangedSubview(createSectionHeader(title: "DATA PEMOHON", iconName: "building.2.fill")) stack.addArrangedSubview(createDivider()) // Company Name let compName = UILabel() compName.font = UIFont.systemFont(ofSize: 15, weight: .bold) compName.textColor = UIColor(red: 26/255, green: 29/255, blue: 38/255, alpha: 1.0) compName.numberOfLines = 0 compName.text = data.cust_name.isEmpty ? "-" : data.cust_name stack.addArrangedSubview(compName) // Details Box let box = UIView() box.backgroundColor = UIColor(red: 248/255, green: 250/255, blue: 253/255, alpha: 1.0) box.layer.cornerRadius = 10 box.layer.borderWidth = 1 box.layer.borderColor = UIColor(red: 235/255, green: 240/255, blue: 248/255, alpha: 1.0).cgColor let boxStack = UIStackView() boxStack.translatesAutoresizingMaskIntoConstraints = false boxStack.axis = .vertical boxStack.spacing = 10 box.addSubview(boxStack) boxStack.addArrangedSubview(createKeyValueRow(key: "NIB (Nomor Induk Berusaha)", value: data.nib.isEmpty ? "-" : data.nib)) boxStack.addArrangedSubview(createKeyValueRow(key: "ID Pelanggan (Cust ID)", value: "\(data.cust_id)")) boxStack.addArrangedSubview(createKeyValueRow(key: "ID Proyek", value: data.id_proyek.isEmpty ? "-" : data.id_proyek)) NSLayoutConstraint.activate([ boxStack.topAnchor.constraint(equalTo: box.topAnchor, constant: 12), boxStack.bottomAnchor.constraint(equalTo: box.bottomAnchor, constant: -12), boxStack.leadingAnchor.constraint(equalTo: box.leadingAnchor, constant: 12), boxStack.trailingAnchor.constraint(equalTo: box.trailingAnchor, constant: -12) ]) stack.addArrangedSubview(box) NSLayoutConstraint.activate([ stack.topAnchor.constraint(equalTo: card.topAnchor, constant: 16), stack.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -16), stack.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16), stack.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16) ]) return card } // 3. Data Perangkat Card private func createPerangkatCard(data: Aplikasi) -> UIView { let card = createCardContainer() let stack = UIStackView() stack.translatesAutoresizingMaskIntoConstraints = false stack.axis = .vertical stack.spacing = 12 card.addSubview(stack) // Section Header stack.addArrangedSubview(createSectionHeader(title: "DATA PERANGKAT", iconName: "antenna.radiowaves.left.and.right")) stack.addArrangedSubview(createDivider()) // Device Name let eqpName = UILabel() eqpName.font = UIFont.systemFont(ofSize: 15, weight: .bold) eqpName.textColor = UIColor(red: 26/255, green: 29/255, blue: 38/255, alpha: 1.0) eqpName.numberOfLines = 0 eqpName.text = data.eqp_name.isEmpty ? "-" : data.eqp_name stack.addArrangedSubview(eqpName) // Spec 2x2 Grid let row1 = UIStackView() row1.axis = .horizontal row1.distribution = .fillEqually row1.spacing = 10 row1.addArrangedSubview(createMiniInfoBox(title: "MERK", value: data.merk.isEmpty ? "-" : data.merk)) row1.addArrangedSubview(createMiniInfoBox(title: "MODEL", value: data.model.isEmpty ? "-" : data.model)) stack.addArrangedSubview(row1) let row2 = UIStackView() row2.axis = .horizontal row2.distribution = .fillEqually row2.spacing = 10 row2.addArrangedSubview(createMiniInfoBox(title: "BRAND", value: data.brand.isEmpty ? "-" : data.brand)) row2.addArrangedSubview(createMiniInfoBox(title: "JENIS PERMOHONAN", value: "Sertifikasi DJID")) stack.addArrangedSubview(row2) NSLayoutConstraint.activate([ stack.topAnchor.constraint(equalTo: card.topAnchor, constant: 16), stack.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -16), stack.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16), stack.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16) ]) return card } // 4. Disposisi & Status Card private func createDisposisiCard(data: Aplikasi) -> UIView { let card = createCardContainer() let stack = UIStackView() stack.translatesAutoresizingMaskIntoConstraints = false stack.axis = .vertical stack.spacing = 12 card.addSubview(stack) stack.addArrangedSubview(createSectionHeader(title: "DISPOSISI & STATUS PROSES", iconName: "arrow.triangle.branch")) stack.addArrangedSubview(createDivider()) let box = UIView() box.backgroundColor = UIColor(red: 248/255, green: 250/255, blue: 253/255, alpha: 1.0) box.layer.cornerRadius = 10 box.layer.borderWidth = 1 box.layer.borderColor = UIColor(red: 235/255, green: 240/255, blue: 248/255, alpha: 1.0).cgColor let boxStack = UIStackView() boxStack.translatesAutoresizingMaskIntoConstraints = false boxStack.axis = .vertical boxStack.spacing = 10 box.addSubview(boxStack) boxStack.addArrangedSubview(createKeyValueRow(key: "Petugas Disposisi", value: data.disposisi.isEmpty ? "-" : data.disposisi)) boxStack.addArrangedSubview(createKeyValueRow(key: "Status Saat Ini", value: data.StatusStr.isEmpty ? "-" : data.StatusStr)) boxStack.addArrangedSubview(createKeyValueRow(key: "Kode Status", value: data.status.isEmpty ? "-" : data.status)) boxStack.addArrangedSubview(createKeyValueRow(key: "ID Permohonan Sistem", value: data.id.isEmpty ? "-" : data.id)) NSLayoutConstraint.activate([ boxStack.topAnchor.constraint(equalTo: box.topAnchor, constant: 12), boxStack.bottomAnchor.constraint(equalTo: box.bottomAnchor, constant: -12), boxStack.leadingAnchor.constraint(equalTo: box.leadingAnchor, constant: 12), boxStack.trailingAnchor.constraint(equalTo: box.trailingAnchor, constant: -12) ]) stack.addArrangedSubview(box) NSLayoutConstraint.activate([ stack.topAnchor.constraint(equalTo: card.topAnchor, constant: 16), stack.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -16), stack.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16), stack.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16) ]) return card } // MARK: - Reusable UI Helpers private func createCardContainer() -> UIView { let card = UIView() card.backgroundColor = .white card.layer.cornerRadius = 16 card.layer.borderWidth = 1 card.layer.borderColor = UIColor(red: 228/255, green: 235/255, blue: 245/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.05 return card } private func createDivider() -> UIView { let div = UIView() div.translatesAutoresizingMaskIntoConstraints = false div.backgroundColor = UIColor(red: 240/255, green: 244/255, blue: 250/255, alpha: 1.0) div.heightAnchor.constraint(equalToConstant: 1).isActive = true return div } private func createSectionHeader(title: String, iconName: String) -> UIView { let stack = UIStackView() stack.axis = .horizontal stack.spacing = 8 stack.alignment = .center let icon = UIImageView(image: UIImage(systemName: iconName)) icon.tintColor = UIColor.siraniPrimaryBlue icon.contentMode = .scaleAspectFit icon.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ icon.widthAnchor.constraint(equalToConstant: 16), icon.heightAnchor.constraint(equalToConstant: 16) ]) stack.addArrangedSubview(icon) let lbl = UILabel() lbl.font = UIFont.systemFont(ofSize: 12, weight: .bold) lbl.textColor = UIColor.siraniPrimaryBlue lbl.text = title stack.addArrangedSubview(lbl) return stack } private func createMiniInfoBox(title: String, value: String) -> UIView { let box = UIView() box.backgroundColor = UIColor(red: 248/255, green: 250/255, blue: 253/255, alpha: 1.0) box.layer.cornerRadius = 10 box.layer.borderWidth = 1 box.layer.borderColor = UIColor(red: 235/255, green: 240/255, blue: 248/255, alpha: 1.0).cgColor let stack = UIStackView() stack.translatesAutoresizingMaskIntoConstraints = false stack.axis = .vertical stack.spacing = 3 box.addSubview(stack) let lblTitle = UILabel() lblTitle.font = UIFont.systemFont(ofSize: 9, weight: .bold) lblTitle.textColor = UIColor(red: 140/255, green: 155/255, blue: 175/255, alpha: 1.0) lblTitle.text = title stack.addArrangedSubview(lblTitle) let lblVal = UILabel() lblVal.font = UIFont.systemFont(ofSize: 12, weight: .semibold) lblVal.textColor = UIColor(red: 40/255, green: 50/255, blue: 65/255, alpha: 1.0) lblVal.numberOfLines = 2 lblVal.text = value stack.addArrangedSubview(lblVal) NSLayoutConstraint.activate([ stack.topAnchor.constraint(equalTo: box.topAnchor, constant: 8), stack.bottomAnchor.constraint(equalTo: box.bottomAnchor, constant: -8), stack.leadingAnchor.constraint(equalTo: box.leadingAnchor, constant: 10), stack.trailingAnchor.constraint(equalTo: box.trailingAnchor, constant: -10) ]) return box } private func createKeyValueRow(key: String, value: String) -> UIView { let stack = UIStackView() stack.axis = .vertical stack.spacing = 2 let k = UILabel() k.font = UIFont.systemFont(ofSize: 10, weight: .bold) k.textColor = UIColor(red: 140/255, green: 155/255, blue: 175/255, alpha: 1.0) k.text = key stack.addArrangedSubview(k) let v = UILabel() v.font = UIFont.systemFont(ofSize: 13, weight: .semibold) v.textColor = UIColor(red: 35/255, green: 45/255, blue: 60/255, alpha: 1.0) v.numberOfLines = 0 v.text = value stack.addArrangedSubview(v) return stack } private func formatDateString(_ dateStr: String) -> String { guard !dateStr.isEmpty, dateStr != "-" else { return "-" } // Try ISO 8601 parsing first let isoFormatter = ISO8601DateFormatter() isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] var date = isoFormatter.date(from: dateStr) if date == nil { isoFormatter.formatOptions = [.withInternetDateTime] date = isoFormatter.date(from: dateStr) } if let d = date { let displayFormatter = DateFormatter() displayFormatter.locale = Locale(identifier: "id_ID") displayFormatter.dateFormat = "dd MMM yyyy, HH:mm" return displayFormatter.string(from: d) + " WIB" } // Fallback for simple space-separated date strings let parts = dateStr.split(separator: "T") if let first = parts.first { return String(first) } return dateStr } // MARK: - Actions @objc private func onBackTap() { dismiss(animated: true, completion: nil) } @objc private func onCopyIdIzin() { guard let data = aplikasiData, !data.id_izin.isEmpty else { return } UIPasteboard.general.string = data.id_izin let generator = UIImpactFeedbackGenerator(style: .medium) generator.impactOccurred() showToast(message: "ID Izin berhasil disalin") } @objc private func onShareTap() { guard let data = aplikasiData else { return } let text = """ Detail Permohonan Aplikasi SIRANI APL ID: \(data.apl_id) ID Izin: \(data.id_izin) Pemohon: \(data.cust_name) Perangkat: \(data.eqp_name) Merk/Model: \(data.merk) / \(data.model) Status: \(data.StatusStr) """ let activityVC = UIActivityViewController(activityItems: [text], applicationActivities: nil) if let popover = activityVC.popoverPresentationController { popover.sourceView = btnShare popover.sourceRect = btnShare.bounds } present(activityVC, animated: true, completion: nil) } private func showToast(message: String) { toastLabel.text = message UIView.animate(withDuration: 0.25, animations: { self.toastView.alpha = 1.0 }) { _ in UIView.animate(withDuration: 0.25, delay: 1.5, options: .curveEaseOut, animations: { self.toastView.alpha = 0 }, completion: nil) } } }