DetailAplikasiVC.swift 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. //
  2. // DetailAplikasiVC.swift
  3. // Sirani
  4. //
  5. // Created by Antigravity on 08/09/26.
  6. // Copyright © 2026 PT. Mitra Sinerji Teknoindo. All rights reserved.
  7. //
  8. import UIKit
  9. class DetailAplikasiVC: UIViewController {
  10. // MARK: - Data
  11. var aplikasiData: Aplikasi?
  12. override var preferredStatusBarStyle: UIStatusBarStyle {
  13. return .lightContent
  14. }
  15. // MARK: - UI Components
  16. private let topStatusBarBackground = UIView()
  17. private let toolbarView = UIView()
  18. private let btnBack = UIButton(type: .custom)
  19. private let titleLabel = UILabel()
  20. private let subtitleLabel = UILabel()
  21. private let btnShare = UIButton(type: .custom)
  22. private let scrollView = UIScrollView()
  23. private let contentView = UIView()
  24. private let contentStack = UIStackView()
  25. // Toast Alert
  26. private let toastView = UIView()
  27. private let toastLabel = UILabel()
  28. // MARK: - Lifecycle
  29. override func viewDidLoad() {
  30. super.viewDidLoad()
  31. setupUI()
  32. populateData()
  33. }
  34. override func viewWillAppear(_ animated: Bool) {
  35. super.viewWillAppear(animated)
  36. AppUtility.lockOrientation(.portrait)
  37. }
  38. override func viewWillDisappear(_ animated: Bool) {
  39. super.viewWillDisappear(animated)
  40. AppUtility.lockOrientation(.all)
  41. }
  42. // MARK: - UI Setup
  43. private func setupUI() {
  44. view.backgroundColor = UIColor(red: 247/255, green: 249/255, blue: 252/255, alpha: 1.0)
  45. // 1. Top Status Bar & Toolbar
  46. topStatusBarBackground.translatesAutoresizingMaskIntoConstraints = false
  47. topStatusBarBackground.backgroundColor = UIColor.siraniPrimaryBlue
  48. view.addSubview(topStatusBarBackground)
  49. toolbarView.translatesAutoresizingMaskIntoConstraints = false
  50. toolbarView.backgroundColor = UIColor.siraniPrimaryBlue
  51. view.addSubview(toolbarView)
  52. // Back Button
  53. btnBack.translatesAutoresizingMaskIntoConstraints = false
  54. if let backImg = UIImage(named: "toolbar-back") {
  55. btnBack.setImage(backImg, for: .normal)
  56. } else {
  57. btnBack.setImage(UIImage(systemName: "chevron.backward"), for: .normal)
  58. btnBack.tintColor = .white
  59. }
  60. btnBack.addTarget(self, action: #selector(onBackTap), for: .touchUpInside)
  61. toolbarView.addSubview(btnBack)
  62. // Titles Stack
  63. let titleStack = UIStackView()
  64. titleStack.translatesAutoresizingMaskIntoConstraints = false
  65. titleStack.axis = .vertical
  66. titleStack.spacing = 2
  67. titleStack.alignment = .leading
  68. toolbarView.addSubview(titleStack)
  69. titleLabel.font = UIFont.systemFont(ofSize: 17, weight: .bold)
  70. titleLabel.textColor = .white
  71. titleLabel.text = "Detail Aplikasi"
  72. titleStack.addArrangedSubview(titleLabel)
  73. subtitleLabel.font = UIFont.systemFont(ofSize: 11, weight: .medium)
  74. subtitleLabel.textColor = UIColor.white.withAlphaComponent(0.8)
  75. subtitleLabel.text = "Permohonan Layanan DJID"
  76. titleStack.addArrangedSubview(subtitleLabel)
  77. // Share Button
  78. btnShare.translatesAutoresizingMaskIntoConstraints = false
  79. btnShare.setImage(UIImage(systemName: "square.and.arrow.up"), for: .normal)
  80. btnShare.tintColor = .white
  81. btnShare.addTarget(self, action: #selector(onShareTap), for: .touchUpInside)
  82. toolbarView.addSubview(btnShare)
  83. // 2. ScrollView & ContentView
  84. scrollView.translatesAutoresizingMaskIntoConstraints = false
  85. scrollView.showsVerticalScrollIndicator = true
  86. scrollView.alwaysBounceVertical = true
  87. view.addSubview(scrollView)
  88. contentView.translatesAutoresizingMaskIntoConstraints = false
  89. scrollView.addSubview(contentView)
  90. contentStack.translatesAutoresizingMaskIntoConstraints = false
  91. contentStack.axis = .vertical
  92. contentStack.spacing = 16
  93. contentStack.alignment = .fill
  94. contentStack.distribution = .fill
  95. contentView.addSubview(contentStack)
  96. // 3. Toast Feedback View
  97. toastView.translatesAutoresizingMaskIntoConstraints = false
  98. toastView.backgroundColor = UIColor(red: 26/255, green: 29/255, blue: 38/255, alpha: 0.9)
  99. toastView.layer.cornerRadius = 10
  100. toastView.layer.masksToBounds = true
  101. toastView.alpha = 0
  102. view.addSubview(toastView)
  103. toastLabel.translatesAutoresizingMaskIntoConstraints = false
  104. toastLabel.font = UIFont.systemFont(ofSize: 12, weight: .semibold)
  105. toastLabel.textColor = .white
  106. toastLabel.textAlignment = .center
  107. toastView.addSubview(toastLabel)
  108. // Layout Constraints
  109. NSLayoutConstraint.activate([
  110. // Status Bar Background
  111. topStatusBarBackground.topAnchor.constraint(equalTo: view.topAnchor),
  112. topStatusBarBackground.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  113. topStatusBarBackground.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  114. topStatusBarBackground.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
  115. // Toolbar
  116. toolbarView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
  117. toolbarView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  118. toolbarView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  119. toolbarView.heightAnchor.constraint(equalToConstant: 56),
  120. btnBack.leadingAnchor.constraint(equalTo: toolbarView.leadingAnchor, constant: 16),
  121. btnBack.centerYAnchor.constraint(equalTo: toolbarView.centerYAnchor),
  122. btnBack.widthAnchor.constraint(equalToConstant: 24),
  123. btnBack.heightAnchor.constraint(equalToConstant: 24),
  124. titleStack.leadingAnchor.constraint(equalTo: btnBack.trailingAnchor, constant: 16),
  125. titleStack.trailingAnchor.constraint(equalTo: btnShare.leadingAnchor, constant: -12),
  126. titleStack.centerYAnchor.constraint(equalTo: toolbarView.centerYAnchor),
  127. btnShare.trailingAnchor.constraint(equalTo: toolbarView.trailingAnchor, constant: -16),
  128. btnShare.centerYAnchor.constraint(equalTo: toolbarView.centerYAnchor),
  129. btnShare.widthAnchor.constraint(equalToConstant: 28),
  130. btnShare.heightAnchor.constraint(equalToConstant: 28),
  131. // ScrollView
  132. scrollView.topAnchor.constraint(equalTo: toolbarView.bottomAnchor),
  133. scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  134. scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  135. scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
  136. contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
  137. contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
  138. contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
  139. contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
  140. contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
  141. contentStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16),
  142. contentStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
  143. contentStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
  144. contentStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -24),
  145. // Toast
  146. toastView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
  147. toastView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20),
  148. toastLabel.topAnchor.constraint(equalTo: toastView.topAnchor, constant: 10),
  149. toastLabel.bottomAnchor.constraint(equalTo: toastView.bottomAnchor, constant: -10),
  150. toastLabel.leadingAnchor.constraint(equalTo: toastView.leadingAnchor, constant: 16),
  151. toastLabel.trailingAnchor.constraint(equalTo: toastView.trailingAnchor, constant: -16)
  152. ])
  153. }
  154. // MARK: - Populate Data
  155. private func populateData() {
  156. guard let data = aplikasiData else { return }
  157. subtitleLabel.text = "APL ID: \(data.apl_id)"
  158. // 1. Hero Card: Status & Identitas Aplikasi
  159. contentStack.addArrangedSubview(createHeroCard(data: data))
  160. // 2. Data Pemohon (Perusahaan)
  161. contentStack.addArrangedSubview(createPemohonCard(data: data))
  162. // 3. Data Perangkat
  163. contentStack.addArrangedSubview(createPerangkatCard(data: data))
  164. // 4. Disposisi & Status Proses
  165. contentStack.addArrangedSubview(createDisposisiCard(data: data))
  166. // 5. Close Button
  167. let btnClose = UIButton(type: .system)
  168. btnClose.translatesAutoresizingMaskIntoConstraints = false
  169. btnClose.setTitle("Kembali ke Daftar Aplikasi", for: .normal)
  170. btnClose.setTitleColor(.white, for: .normal)
  171. btnClose.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .bold)
  172. btnClose.backgroundColor = UIColor.siraniPrimaryBlue
  173. btnClose.layer.cornerRadius = 14
  174. btnClose.layer.shadowColor = UIColor.siraniPrimaryBlue.cgColor
  175. btnClose.layer.shadowOffset = CGSize(width: 0, height: 4)
  176. btnClose.layer.shadowRadius = 8
  177. btnClose.layer.shadowOpacity = 0.25
  178. btnClose.heightAnchor.constraint(equalToConstant: 48).isActive = true
  179. btnClose.addTarget(self, action: #selector(onBackTap), for: .touchUpInside)
  180. contentStack.addArrangedSubview(btnClose)
  181. }
  182. // MARK: - Card Builders
  183. // 1. Hero Card (APL ID, ID Izin, Status, Resi)
  184. private func createHeroCard(data: Aplikasi) -> UIView {
  185. let card = createCardContainer()
  186. let stack = UIStackView()
  187. stack.translatesAutoresizingMaskIntoConstraints = false
  188. stack.axis = .vertical
  189. stack.spacing = 14
  190. card.addSubview(stack)
  191. // Header Row: Badge Icon + APL ID + Status Pill
  192. let headerRow = UIStackView()
  193. headerRow.axis = .horizontal
  194. headerRow.alignment = .center
  195. headerRow.spacing = 10
  196. let iconBadge = UIView()
  197. iconBadge.translatesAutoresizingMaskIntoConstraints = false
  198. iconBadge.backgroundColor = UIColor.siraniSoftBlue
  199. iconBadge.layer.cornerRadius = 10
  200. iconBadge.layer.masksToBounds = true
  201. let icon = UIImageView(image: UIImage(systemName: "app.badge.fill"))
  202. icon.tintColor = UIColor.siraniPrimaryBlue
  203. icon.contentMode = .scaleAspectFit
  204. icon.translatesAutoresizingMaskIntoConstraints = false
  205. iconBadge.addSubview(icon)
  206. NSLayoutConstraint.activate([
  207. iconBadge.widthAnchor.constraint(equalToConstant: 38),
  208. iconBadge.heightAnchor.constraint(equalToConstant: 38),
  209. icon.centerXAnchor.constraint(equalTo: iconBadge.centerXAnchor),
  210. icon.centerYAnchor.constraint(equalTo: iconBadge.centerYAnchor),
  211. icon.widthAnchor.constraint(equalToConstant: 20),
  212. icon.heightAnchor.constraint(equalToConstant: 20)
  213. ])
  214. headerRow.addArrangedSubview(iconBadge)
  215. let idStack = UIStackView()
  216. idStack.axis = .vertical
  217. idStack.spacing = 2
  218. let aplTitle = UILabel()
  219. aplTitle.font = UIFont.systemFont(ofSize: 10, weight: .bold)
  220. aplTitle.textColor = UIColor(red: 140/255, green: 155/255, blue: 175/255, alpha: 1.0)
  221. aplTitle.text = "APL ID"
  222. idStack.addArrangedSubview(aplTitle)
  223. let aplVal = UILabel()
  224. aplVal.font = UIFont.systemFont(ofSize: 18, weight: .bold)
  225. aplVal.textColor = UIColor.siraniPrimaryBlue
  226. aplVal.text = "\(data.apl_id)"
  227. idStack.addArrangedSubview(aplVal)
  228. headerRow.addArrangedSubview(idStack)
  229. let spacer = UIView()
  230. spacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
  231. headerRow.addArrangedSubview(spacer)
  232. // Status Pill
  233. let statusPill = UIView()
  234. statusPill.translatesAutoresizingMaskIntoConstraints = false
  235. statusPill.layer.cornerRadius = 8
  236. statusPill.layer.masksToBounds = true
  237. let statusStr = data.StatusStr.trimmingCharacters(in: .whitespacesAndNewlines)
  238. let statusColorHex = data.StatusBgColor.trimmingCharacters(in: .whitespacesAndNewlines)
  239. let statusLabel = UILabel()
  240. statusLabel.translatesAutoresizingMaskIntoConstraints = false
  241. statusLabel.font = UIFont.systemFont(ofSize: 11, weight: .bold)
  242. statusLabel.text = statusStr.isEmpty ? "PERMOHONAN" : statusStr
  243. if let customColor = UIColor(hexString: statusColorHex), !statusColorHex.isEmpty {
  244. statusPill.backgroundColor = customColor.withAlphaComponent(0.15)
  245. statusLabel.textColor = customColor
  246. } else {
  247. statusPill.backgroundColor = UIColor(red: 230/255, green: 244/255, blue: 234/255, alpha: 1.0)
  248. statusLabel.textColor = UIColor(red: 19/255, green: 115/255, blue: 51/255, alpha: 1.0)
  249. }
  250. statusPill.addSubview(statusLabel)
  251. NSLayoutConstraint.activate([
  252. statusLabel.topAnchor.constraint(equalTo: statusPill.topAnchor, constant: 6),
  253. statusLabel.bottomAnchor.constraint(equalTo: statusPill.bottomAnchor, constant: -6),
  254. statusLabel.leadingAnchor.constraint(equalTo: statusPill.leadingAnchor, constant: 10),
  255. statusLabel.trailingAnchor.constraint(equalTo: statusPill.trailingAnchor, constant: -10)
  256. ])
  257. headerRow.addArrangedSubview(statusPill)
  258. stack.addArrangedSubview(headerRow)
  259. stack.addArrangedSubview(createDivider())
  260. // ID Izin Box with Copy Action
  261. let idIzinBox = UIView()
  262. idIzinBox.backgroundColor = UIColor(red: 248/255, green: 250/255, blue: 254/255, alpha: 1.0)
  263. idIzinBox.layer.cornerRadius = 10
  264. idIzinBox.layer.borderWidth = 1
  265. idIzinBox.layer.borderColor = UIColor(red: 230/255, green: 237/255, blue: 248/255, alpha: 1.0).cgColor
  266. let idIzinStack = UIStackView()
  267. idIzinStack.translatesAutoresizingMaskIntoConstraints = false
  268. idIzinStack.axis = .horizontal
  269. idIzinStack.alignment = .center
  270. idIzinStack.spacing = 10
  271. idIzinBox.addSubview(idIzinStack)
  272. let idIzinInner = UIStackView()
  273. idIzinInner.axis = .vertical
  274. idIzinInner.spacing = 2
  275. let idIzinTitle = UILabel()
  276. idIzinTitle.font = UIFont.systemFont(ofSize: 10, weight: .bold)
  277. idIzinTitle.textColor = UIColor(red: 140/255, green: 155/255, blue: 175/255, alpha: 1.0)
  278. idIzinTitle.text = "ID IZIN PERMOHONAN"
  279. idIzinInner.addArrangedSubview(idIzinTitle)
  280. let idIzinVal = UILabel()
  281. idIzinVal.font = UIFont.systemFont(ofSize: 13, weight: .bold)
  282. idIzinVal.textColor = UIColor(red: 30/255, green: 40/255, blue: 60/255, alpha: 1.0)
  283. idIzinVal.text = data.id_izin.isEmpty ? "-" : data.id_izin
  284. idIzinVal.numberOfLines = 1
  285. idIzinInner.addArrangedSubview(idIzinVal)
  286. idIzinStack.addArrangedSubview(idIzinInner)
  287. let copySpacer = UIView()
  288. copySpacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
  289. idIzinStack.addArrangedSubview(copySpacer)
  290. // Copy Button
  291. let btnCopy = UIButton(type: .system)
  292. btnCopy.setImage(UIImage(systemName: "doc.on.doc"), for: .normal)
  293. btnCopy.setTitle(" Salin", for: .normal)
  294. btnCopy.titleLabel?.font = UIFont.systemFont(ofSize: 12, weight: .bold)
  295. btnCopy.tintColor = UIColor.siraniPrimaryBlue
  296. btnCopy.backgroundColor = UIColor.white
  297. btnCopy.layer.cornerRadius = 8
  298. btnCopy.layer.borderWidth = 1
  299. btnCopy.layer.borderColor = UIColor(red: 220/255, green: 230/255, blue: 245/255, alpha: 1.0).cgColor
  300. btnCopy.contentEdgeInsets = UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10)
  301. btnCopy.addTarget(self, action: #selector(onCopyIdIzin), for: .touchUpInside)
  302. idIzinStack.addArrangedSubview(btnCopy)
  303. NSLayoutConstraint.activate([
  304. idIzinStack.topAnchor.constraint(equalTo: idIzinBox.topAnchor, constant: 10),
  305. idIzinStack.bottomAnchor.constraint(equalTo: idIzinBox.bottomAnchor, constant: -10),
  306. idIzinStack.leadingAnchor.constraint(equalTo: idIzinBox.leadingAnchor, constant: 12),
  307. idIzinStack.trailingAnchor.constraint(equalTo: idIzinBox.trailingAnchor, constant: -12)
  308. ])
  309. stack.addArrangedSubview(idIzinBox)
  310. // 2-Column Resi Box
  311. let resiStack = UIStackView()
  312. resiStack.axis = .horizontal
  313. resiStack.distribution = .fillEqually
  314. resiStack.spacing = 10
  315. let resiNoBox = createMiniInfoBox(title: "NO. RESI", value: data.resi_no.isEmpty ? "-" : data.resi_no)
  316. let resiDateFormatted = formatDateString(data.resi_date)
  317. let resiDateBox = createMiniInfoBox(title: "TANGGAL RESI", value: resiDateFormatted)
  318. resiStack.addArrangedSubview(resiNoBox)
  319. resiStack.addArrangedSubview(resiDateBox)
  320. stack.addArrangedSubview(resiStack)
  321. NSLayoutConstraint.activate([
  322. stack.topAnchor.constraint(equalTo: card.topAnchor, constant: 16),
  323. stack.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -16),
  324. stack.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
  325. stack.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16)
  326. ])
  327. return card
  328. }
  329. // 2. Data Pemohon Card
  330. private func createPemohonCard(data: Aplikasi) -> UIView {
  331. let card = createCardContainer()
  332. let stack = UIStackView()
  333. stack.translatesAutoresizingMaskIntoConstraints = false
  334. stack.axis = .vertical
  335. stack.spacing = 12
  336. card.addSubview(stack)
  337. // Section Header
  338. stack.addArrangedSubview(createSectionHeader(title: "DATA PEMOHON", iconName: "building.2.fill"))
  339. stack.addArrangedSubview(createDivider())
  340. // Company Name
  341. let compName = UILabel()
  342. compName.font = UIFont.systemFont(ofSize: 15, weight: .bold)
  343. compName.textColor = UIColor(red: 26/255, green: 29/255, blue: 38/255, alpha: 1.0)
  344. compName.numberOfLines = 0
  345. compName.text = data.cust_name.isEmpty ? "-" : data.cust_name
  346. stack.addArrangedSubview(compName)
  347. // Details Box
  348. let box = UIView()
  349. box.backgroundColor = UIColor(red: 248/255, green: 250/255, blue: 253/255, alpha: 1.0)
  350. box.layer.cornerRadius = 10
  351. box.layer.borderWidth = 1
  352. box.layer.borderColor = UIColor(red: 235/255, green: 240/255, blue: 248/255, alpha: 1.0).cgColor
  353. let boxStack = UIStackView()
  354. boxStack.translatesAutoresizingMaskIntoConstraints = false
  355. boxStack.axis = .vertical
  356. boxStack.spacing = 10
  357. box.addSubview(boxStack)
  358. boxStack.addArrangedSubview(createKeyValueRow(key: "NIB (Nomor Induk Berusaha)", value: data.nib.isEmpty ? "-" : data.nib))
  359. boxStack.addArrangedSubview(createKeyValueRow(key: "ID Pelanggan (Cust ID)", value: "\(data.cust_id)"))
  360. boxStack.addArrangedSubview(createKeyValueRow(key: "ID Proyek", value: data.id_proyek.isEmpty ? "-" : data.id_proyek))
  361. NSLayoutConstraint.activate([
  362. boxStack.topAnchor.constraint(equalTo: box.topAnchor, constant: 12),
  363. boxStack.bottomAnchor.constraint(equalTo: box.bottomAnchor, constant: -12),
  364. boxStack.leadingAnchor.constraint(equalTo: box.leadingAnchor, constant: 12),
  365. boxStack.trailingAnchor.constraint(equalTo: box.trailingAnchor, constant: -12)
  366. ])
  367. stack.addArrangedSubview(box)
  368. NSLayoutConstraint.activate([
  369. stack.topAnchor.constraint(equalTo: card.topAnchor, constant: 16),
  370. stack.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -16),
  371. stack.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
  372. stack.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16)
  373. ])
  374. return card
  375. }
  376. // 3. Data Perangkat Card
  377. private func createPerangkatCard(data: Aplikasi) -> UIView {
  378. let card = createCardContainer()
  379. let stack = UIStackView()
  380. stack.translatesAutoresizingMaskIntoConstraints = false
  381. stack.axis = .vertical
  382. stack.spacing = 12
  383. card.addSubview(stack)
  384. // Section Header
  385. stack.addArrangedSubview(createSectionHeader(title: "DATA PERANGKAT", iconName: "antenna.radiowaves.left.and.right"))
  386. stack.addArrangedSubview(createDivider())
  387. // Device Name
  388. let eqpName = UILabel()
  389. eqpName.font = UIFont.systemFont(ofSize: 15, weight: .bold)
  390. eqpName.textColor = UIColor(red: 26/255, green: 29/255, blue: 38/255, alpha: 1.0)
  391. eqpName.numberOfLines = 0
  392. eqpName.text = data.eqp_name.isEmpty ? "-" : data.eqp_name
  393. stack.addArrangedSubview(eqpName)
  394. // Spec 2x2 Grid
  395. let row1 = UIStackView()
  396. row1.axis = .horizontal
  397. row1.distribution = .fillEqually
  398. row1.spacing = 10
  399. row1.addArrangedSubview(createMiniInfoBox(title: "MERK", value: data.merk.isEmpty ? "-" : data.merk))
  400. row1.addArrangedSubview(createMiniInfoBox(title: "MODEL", value: data.model.isEmpty ? "-" : data.model))
  401. stack.addArrangedSubview(row1)
  402. let row2 = UIStackView()
  403. row2.axis = .horizontal
  404. row2.distribution = .fillEqually
  405. row2.spacing = 10
  406. row2.addArrangedSubview(createMiniInfoBox(title: "BRAND", value: data.brand.isEmpty ? "-" : data.brand))
  407. row2.addArrangedSubview(createMiniInfoBox(title: "JENIS PERMOHONAN", value: "Sertifikasi DJID"))
  408. stack.addArrangedSubview(row2)
  409. NSLayoutConstraint.activate([
  410. stack.topAnchor.constraint(equalTo: card.topAnchor, constant: 16),
  411. stack.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -16),
  412. stack.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
  413. stack.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16)
  414. ])
  415. return card
  416. }
  417. // 4. Disposisi & Status Card
  418. private func createDisposisiCard(data: Aplikasi) -> UIView {
  419. let card = createCardContainer()
  420. let stack = UIStackView()
  421. stack.translatesAutoresizingMaskIntoConstraints = false
  422. stack.axis = .vertical
  423. stack.spacing = 12
  424. card.addSubview(stack)
  425. stack.addArrangedSubview(createSectionHeader(title: "DISPOSISI & STATUS PROSES", iconName: "arrow.triangle.branch"))
  426. stack.addArrangedSubview(createDivider())
  427. let box = UIView()
  428. box.backgroundColor = UIColor(red: 248/255, green: 250/255, blue: 253/255, alpha: 1.0)
  429. box.layer.cornerRadius = 10
  430. box.layer.borderWidth = 1
  431. box.layer.borderColor = UIColor(red: 235/255, green: 240/255, blue: 248/255, alpha: 1.0).cgColor
  432. let boxStack = UIStackView()
  433. boxStack.translatesAutoresizingMaskIntoConstraints = false
  434. boxStack.axis = .vertical
  435. boxStack.spacing = 10
  436. box.addSubview(boxStack)
  437. boxStack.addArrangedSubview(createKeyValueRow(key: "Petugas Disposisi", value: data.disposisi.isEmpty ? "-" : data.disposisi))
  438. boxStack.addArrangedSubview(createKeyValueRow(key: "Status Saat Ini", value: data.StatusStr.isEmpty ? "-" : data.StatusStr))
  439. boxStack.addArrangedSubview(createKeyValueRow(key: "Kode Status", value: data.status.isEmpty ? "-" : data.status))
  440. boxStack.addArrangedSubview(createKeyValueRow(key: "ID Permohonan Sistem", value: data.id.isEmpty ? "-" : data.id))
  441. NSLayoutConstraint.activate([
  442. boxStack.topAnchor.constraint(equalTo: box.topAnchor, constant: 12),
  443. boxStack.bottomAnchor.constraint(equalTo: box.bottomAnchor, constant: -12),
  444. boxStack.leadingAnchor.constraint(equalTo: box.leadingAnchor, constant: 12),
  445. boxStack.trailingAnchor.constraint(equalTo: box.trailingAnchor, constant: -12)
  446. ])
  447. stack.addArrangedSubview(box)
  448. NSLayoutConstraint.activate([
  449. stack.topAnchor.constraint(equalTo: card.topAnchor, constant: 16),
  450. stack.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -16),
  451. stack.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
  452. stack.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16)
  453. ])
  454. return card
  455. }
  456. // MARK: - Reusable UI Helpers
  457. private func createCardContainer() -> UIView {
  458. let card = UIView()
  459. card.backgroundColor = .white
  460. card.layer.cornerRadius = 16
  461. card.layer.borderWidth = 1
  462. card.layer.borderColor = UIColor(red: 228/255, green: 235/255, blue: 245/255, alpha: 1.0).cgColor
  463. card.layer.shadowColor = UIColor.black.cgColor
  464. card.layer.shadowOffset = CGSize(width: 0, height: 2)
  465. card.layer.shadowRadius = 8
  466. card.layer.shadowOpacity = 0.05
  467. return card
  468. }
  469. private func createDivider() -> UIView {
  470. let div = UIView()
  471. div.translatesAutoresizingMaskIntoConstraints = false
  472. div.backgroundColor = UIColor(red: 240/255, green: 244/255, blue: 250/255, alpha: 1.0)
  473. div.heightAnchor.constraint(equalToConstant: 1).isActive = true
  474. return div
  475. }
  476. private func createSectionHeader(title: String, iconName: String) -> UIView {
  477. let stack = UIStackView()
  478. stack.axis = .horizontal
  479. stack.spacing = 8
  480. stack.alignment = .center
  481. let icon = UIImageView(image: UIImage(systemName: iconName))
  482. icon.tintColor = UIColor.siraniPrimaryBlue
  483. icon.contentMode = .scaleAspectFit
  484. icon.translatesAutoresizingMaskIntoConstraints = false
  485. NSLayoutConstraint.activate([
  486. icon.widthAnchor.constraint(equalToConstant: 16),
  487. icon.heightAnchor.constraint(equalToConstant: 16)
  488. ])
  489. stack.addArrangedSubview(icon)
  490. let lbl = UILabel()
  491. lbl.font = UIFont.systemFont(ofSize: 12, weight: .bold)
  492. lbl.textColor = UIColor.siraniPrimaryBlue
  493. lbl.text = title
  494. stack.addArrangedSubview(lbl)
  495. return stack
  496. }
  497. private func createMiniInfoBox(title: String, value: String) -> UIView {
  498. let box = UIView()
  499. box.backgroundColor = UIColor(red: 248/255, green: 250/255, blue: 253/255, alpha: 1.0)
  500. box.layer.cornerRadius = 10
  501. box.layer.borderWidth = 1
  502. box.layer.borderColor = UIColor(red: 235/255, green: 240/255, blue: 248/255, alpha: 1.0).cgColor
  503. let stack = UIStackView()
  504. stack.translatesAutoresizingMaskIntoConstraints = false
  505. stack.axis = .vertical
  506. stack.spacing = 3
  507. box.addSubview(stack)
  508. let lblTitle = UILabel()
  509. lblTitle.font = UIFont.systemFont(ofSize: 9, weight: .bold)
  510. lblTitle.textColor = UIColor(red: 140/255, green: 155/255, blue: 175/255, alpha: 1.0)
  511. lblTitle.text = title
  512. stack.addArrangedSubview(lblTitle)
  513. let lblVal = UILabel()
  514. lblVal.font = UIFont.systemFont(ofSize: 12, weight: .semibold)
  515. lblVal.textColor = UIColor(red: 40/255, green: 50/255, blue: 65/255, alpha: 1.0)
  516. lblVal.numberOfLines = 2
  517. lblVal.text = value
  518. stack.addArrangedSubview(lblVal)
  519. NSLayoutConstraint.activate([
  520. stack.topAnchor.constraint(equalTo: box.topAnchor, constant: 8),
  521. stack.bottomAnchor.constraint(equalTo: box.bottomAnchor, constant: -8),
  522. stack.leadingAnchor.constraint(equalTo: box.leadingAnchor, constant: 10),
  523. stack.trailingAnchor.constraint(equalTo: box.trailingAnchor, constant: -10)
  524. ])
  525. return box
  526. }
  527. private func createKeyValueRow(key: String, value: String) -> UIView {
  528. let stack = UIStackView()
  529. stack.axis = .vertical
  530. stack.spacing = 2
  531. let k = UILabel()
  532. k.font = UIFont.systemFont(ofSize: 10, weight: .bold)
  533. k.textColor = UIColor(red: 140/255, green: 155/255, blue: 175/255, alpha: 1.0)
  534. k.text = key
  535. stack.addArrangedSubview(k)
  536. let v = UILabel()
  537. v.font = UIFont.systemFont(ofSize: 13, weight: .semibold)
  538. v.textColor = UIColor(red: 35/255, green: 45/255, blue: 60/255, alpha: 1.0)
  539. v.numberOfLines = 0
  540. v.text = value
  541. stack.addArrangedSubview(v)
  542. return stack
  543. }
  544. private func formatDateString(_ dateStr: String) -> String {
  545. guard !dateStr.isEmpty, dateStr != "-" else { return "-" }
  546. // Try ISO 8601 parsing first
  547. let isoFormatter = ISO8601DateFormatter()
  548. isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
  549. var date = isoFormatter.date(from: dateStr)
  550. if date == nil {
  551. isoFormatter.formatOptions = [.withInternetDateTime]
  552. date = isoFormatter.date(from: dateStr)
  553. }
  554. if let d = date {
  555. let displayFormatter = DateFormatter()
  556. displayFormatter.locale = Locale(identifier: "id_ID")
  557. displayFormatter.dateFormat = "dd MMM yyyy, HH:mm"
  558. return displayFormatter.string(from: d) + " WIB"
  559. }
  560. // Fallback for simple space-separated date strings
  561. let parts = dateStr.split(separator: "T")
  562. if let first = parts.first {
  563. return String(first)
  564. }
  565. return dateStr
  566. }
  567. // MARK: - Actions
  568. @objc private func onBackTap() {
  569. dismiss(animated: true, completion: nil)
  570. }
  571. @objc private func onCopyIdIzin() {
  572. guard let data = aplikasiData, !data.id_izin.isEmpty else { return }
  573. UIPasteboard.general.string = data.id_izin
  574. let generator = UIImpactFeedbackGenerator(style: .medium)
  575. generator.impactOccurred()
  576. showToast(message: "ID Izin berhasil disalin")
  577. }
  578. @objc private func onShareTap() {
  579. guard let data = aplikasiData else { return }
  580. let text = """
  581. Detail Permohonan Aplikasi SIRANI
  582. APL ID: \(data.apl_id)
  583. ID Izin: \(data.id_izin)
  584. Pemohon: \(data.cust_name)
  585. Perangkat: \(data.eqp_name)
  586. Merk/Model: \(data.merk) / \(data.model)
  587. Status: \(data.StatusStr)
  588. """
  589. let activityVC = UIActivityViewController(activityItems: [text], applicationActivities: nil)
  590. if let popover = activityVC.popoverPresentationController {
  591. popover.sourceView = btnShare
  592. popover.sourceRect = btnShare.bounds
  593. }
  594. present(activityVC, animated: true, completion: nil)
  595. }
  596. private func showToast(message: String) {
  597. toastLabel.text = message
  598. UIView.animate(withDuration: 0.25, animations: {
  599. self.toastView.alpha = 1.0
  600. }) { _ in
  601. UIView.animate(withDuration: 0.25, delay: 1.5, options: .curveEaseOut, animations: {
  602. self.toastView.alpha = 0
  603. }, completion: nil)
  604. }
  605. }
  606. }