Skip to content

Commit

Permalink
[INSPECT-340][FEATURE] - added scroll feature to validation (#341)
Browse files Browse the repository at this point in the history
* feature: added scroll feature to validation
- added emojis to validation errors
- created new alert for validation message in inspection

* change remoteENV
  • Loading branch information
PaulGarewal authored Dec 20, 2024
1 parent 94bf150 commit 4bb2464
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 102 deletions.
8 changes: 6 additions & 2 deletions ipad.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@
A7AE5275237CC3660044DBB7 /* unapproved-cross.json in Resources */ = {isa = PBXBuildFile; fileRef = A7AE5272237CC3660044DBB7 /* unapproved-cross.json */; };
A7AE5276237CC3660044DBB7 /* check-mark-success.json in Resources */ = {isa = PBXBuildFile; fileRef = A7AE5273237CC3660044DBB7 /* check-mark-success.json */; };
A7AE5277237CC3660044DBB7 /* sync-circle.json in Resources */ = {isa = PBXBuildFile; fileRef = A7AE5274237CC3660044DBB7 /* sync-circle.json */; };
E42434792D1499A800C7EF20 /* ScrollAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = E42434782D14999600C7EF20 /* ScrollAlert.swift */; };
F5EB30A127B5C77400F22712 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 29CEC91F23676424003B21B9 /* Main.storyboard */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -467,6 +468,7 @@
A7AE5272237CC3660044DBB7 /* unapproved-cross.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "unapproved-cross.json"; sourceTree = "<group>"; };
A7AE5273237CC3660044DBB7 /* check-mark-success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "check-mark-success.json"; sourceTree = "<group>"; };
A7AE5274237CC3660044DBB7 /* sync-circle.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "sync-circle.json"; sourceTree = "<group>"; };
E42434782D14999600C7EF20 /* ScrollAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollAlert.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -724,6 +726,7 @@
299BF01A23FC9CE9001732CD /* PDFViewer */,
294EF3102370884300EEB3B6 /* Banner */,
29BAC50523676EA400A620F4 /* Alert.swift */,
E42434782D14999600C7EF20 /* ScrollAlert.swift */,
29BAC50F2367984000A620F4 /* GradiantView.swift */,
);
path = UI;
Expand Down Expand Up @@ -1774,6 +1777,7 @@
2907E4B72368D24900946B3F /* InputModal.swift in Sources */,
29547B4A23F0BF4F000173F1 /* SelectedWaterBodyCollectionViewCell.swift in Sources */,
2991969023846AE000634F81 /* ShiftViewController.swift in Sources */,
E42434792D1499A800C7EF20 /* ScrollAlert.swift in Sources */,
29DB01092370EFD60046E605 /* TextAreaInputCollectionViewCell.swift in Sources */,
5C82D3EA2374BCFA00B065BA /* FoundationExtension.swift in Sources */,
29CEC91E23676424003B21B9 /* ViewController.swift in Sources */,
Expand Down Expand Up @@ -2059,7 +2063,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.8.4;
MARKETING_VERSION = 2.8.5;
PRODUCT_BUNDLE_IDENTIFIER = ca.bc.gov.InvasivesBC;
PRODUCT_NAME = Inspect;
PROVISIONING_PROFILE_SPECIFIER = "InvasivesBC Muscles - 2023/24";
Expand Down Expand Up @@ -2089,7 +2093,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.8.4;
MARKETING_VERSION = 2.8.5;
PRODUCT_BUNDLE_IDENTIFIER = ca.bc.gov.InvasivesBC;
PRODUCT_NAME = Inspect;
PROVISIONING_PROFILE_SPECIFIER = "InvasivesBC Muscles - 2023/24";
Expand Down
18 changes: 18 additions & 0 deletions ipad/Utilities/Core/Extensions/UIViewControllerExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,21 @@ extension UIViewController {
return alert
}
}

extension UIViewController {
func topMostViewController() -> UIViewController {
if let presented = self.presentedViewController {
return presented.topMostViewController()
}

if let navigation = self as? UINavigationController {
return navigation.visibleViewController?.topMostViewController() ?? navigation
}

if let tab = self as? UITabBarController {
return tab.selectedViewController?.topMostViewController() ?? tab
}

return self
}
}
53 changes: 15 additions & 38 deletions ipad/Utilities/Core/UI/Alert.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,57 +17,34 @@ class Alert {
and return call back when user makes selection
*/
static func show(title: String, message: String, yes: @escaping()-> Void, no: @escaping()-> Void) {
//self.showDefaultAlert(title: title, message: message, yes: yes, no: no)
self.showCustomAlert(title: title, message: message, yes: yes, no: no);
self.showCustomAlert(title: title, message: message, yes: yes, no: no)
}

/**
Show an alert message with an okay button
*/
static func show(title: String, message: String) {
//self.showDefaultAlert(title: title, message: message)
self.showCustomAlert(title: title, message: message);
ModalAlert.show(title: title, message: message)
}

// MARK: Default iOS alerts
private static func showDefaultAlert(title: String, message: String, yes: @escaping()-> Void, no: @escaping()-> Void) {
DispatchQueue.main.async(execute: {
let alertWindow = UIWindow(frame: UIScreen.main.bounds)
alertWindow.rootViewController = UIViewController()
alertWindow.windowLevel = UIWindow.Level.alert + 1

let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Yes", style: UIAlertAction.Style.default, handler: { action in
return yes();
}))
alert.addAction(UIAlertAction(title: "No", style: UIAlertAction.Style.cancel, handler: { action in
return no();
}))

alertWindow.makeKeyAndVisible()
alertWindow.rootViewController?.present(alert, animated: true, completion: nil)
})
/**
Show a validation alert message for inspections
*/
static func showValidation(title: String, message: String) {
if let topVC = UIApplication.shared.windows.first?.rootViewController?.topMostViewController() {
let alertVC = ScrollableAlertViewController()
alertVC.modalPresentationStyle = .overFullScreen
alertVC.modalTransitionStyle = .crossDissolve
alertVC.configure(title: title, message: message)
topVC.present(alertVC, animated: true)
}
}

private static func showDefaultAlert(title: String, message: String) {
DispatchQueue.main.async(execute: {
let alertWindow = UIWindow(frame: UIScreen.main.bounds)
alertWindow.rootViewController = UIViewController()
alertWindow.windowLevel = UIWindow.Level.alert + 1

let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
let defaultAction2 = UIAlertAction(title: "OK", style: .default, handler: { action in
})
alert.addAction(defaultAction2)

alertWindow.makeKeyAndVisible()
alertWindow.rootViewController?.present(alert, animated: true, completion: nil)
})
}


// MARK: Custom alerts using Modal pod
private static func showCustomAlert(title: String, message: String, yes: @escaping()-> Void, no: @escaping()-> Void) {
ModalAlert.show(title: title, message: message, yes: yes, no: no);
ModalAlert.show(title: title, message: message, yes: yes, no: no)
}

private static func showCustomAlert(title: String, message: String) {
Expand Down
95 changes: 95 additions & 0 deletions ipad/Utilities/Core/UI/ScrollAlert.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//
// ScrollAlert.swift
// ipad
//
// Created by Paul Garewal on 2024-12-19.
// Copyright © 2024 Amir Shayegh. All rights reserved.
//

import UIKit

class ScrollableAlertViewController: UIViewController {

private let titleLabel = UILabel()
private let textView = UITextView()
private let okButton = UIButton(type: .system)

override func viewDidLoad() {
super.viewDidLoad()
setupView()
}

private func setupView() {
// Configure background
view.backgroundColor = UIColor.black.withAlphaComponent(0.5)

// Container for the alert
let alertContainer = UIView()
alertContainer.backgroundColor = .white
alertContainer.layer.cornerRadius = 12
alertContainer.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(alertContainer)

// Title label
titleLabel.font = .boldSystemFont(ofSize: 17)
titleLabel.textColor = .black
titleLabel.numberOfLines = 1
titleLabel.textAlignment = .center
titleLabel.translatesAutoresizingMaskIntoConstraints = false
alertContainer.addSubview(titleLabel)

// Scrollable text view
textView.isEditable = false
textView.isScrollEnabled = true
textView.font = .systemFont(ofSize: 14)
textView.textColor = .black
textView.backgroundColor = .clear
textView.showsVerticalScrollIndicator = true
textView.translatesAutoresizingMaskIntoConstraints = false
alertContainer.addSubview(textView)

// OK button
okButton.setTitle("OK", for: .normal)
okButton.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold)
okButton.setTitleColor(.systemBlue, for: .normal)
okButton.translatesAutoresizingMaskIntoConstraints = false
okButton.contentEdgeInsets = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16)
okButton.backgroundColor = .clear
alertContainer.addSubview(okButton)
okButton.addTarget(self, action: #selector(dismissAlert), for: .touchUpInside)

NSLayoutConstraint.activate([
// Alert container
alertContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor),
alertContainer.centerYAnchor.constraint(equalTo: view.centerYAnchor),
alertContainer.widthAnchor.constraint(equalToConstant: 270),

// Title label
titleLabel.topAnchor.constraint(equalTo: alertContainer.topAnchor, constant: 16),
titleLabel.leadingAnchor.constraint(equalTo: alertContainer.leadingAnchor, constant: 16),
titleLabel.trailingAnchor.constraint(equalTo: alertContainer.trailingAnchor, constant: -16),

// Text view
textView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8),
textView.leadingAnchor.constraint(equalTo: alertContainer.leadingAnchor, constant: 16),
textView.trailingAnchor.constraint(equalTo: alertContainer.trailingAnchor, constant: -16),
textView.heightAnchor.constraint(equalToConstant: 200),

// OK button
okButton.topAnchor.constraint(equalTo: textView.bottomAnchor, constant: 8),
okButton.bottomAnchor.constraint(equalTo: alertContainer.bottomAnchor, constant: -8),
okButton.leadingAnchor.constraint(equalTo: alertContainer.leadingAnchor),
okButton.trailingAnchor.constraint(equalTo: alertContainer.trailingAnchor),
okButton.heightAnchor.constraint(equalToConstant: 44)
])
}

@objc private func dismissAlert() {
dismiss(animated: true)
}

func configure(title: String, message: String) {
titleLabel.text = title
textView.text = message
}
}
91 changes: 41 additions & 50 deletions ipad/ViewControllers/Shift/ShiftViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -290,90 +290,81 @@ class ShiftViewController: BaseViewController {
}

func validationMessage() -> String {
var message: String = ""
guard let model = self.model else { return message }
var counter = 1
var messages: [String] = []
guard let model = self.model else { return "" }

// Group validation messages by category

// Shift Time Validations
if model.startTime.isEmpty {
message = "\(message)\n\(counter)- Missing Shift Start time."
counter += 1
messages.append("⏰ Shift Start time is required")
}

if model.endTime.isEmpty {
message = "\(message)\n\(counter)- Missing Shift End time."
counter += 1
messages.append("⏰ Shift End time is required")
}

// Inspection Count Validations
if model.inspections.count > 0 && model.boatsInspected == false {
message = "\(message)\n\(counter)- You indicated that no boats were inspected, but inspections exist."
counter += 1
messages.append("⚠️ Inspection count mismatch: You indicated no boats were inspected, but inspections exist")
}

if model.inspections.count < 1 && model.boatsInspected == true {
message = "\(message)\n\(counter)- You indicated that boats were inspected but inspections are missing."
counter += 1
messages.append("⚠️ Inspection count mismatch: You indicated boats were inspected but no inspections are recorded")
}

// Station Validations
if model.station.isEmpty {
message = "\(message)\n\(counter)- Please choose a station."
counter += 1
messages.append("📍 Station selection is required")
}

if model.stationComments.isEmpty && ShiftModel.stationRequired(model.station) {
message = "\(message)\n\(counter)- Please add station information."
counter += 1
messages.append("📍 Station information is required")
}

for inspection in model.inspections {
// Inspection Detail Validations
for (index, inspection) in model.inspections.enumerated() {
if inspection.inspectionTime.isEmpty {
message = "\(message)\n\(counter)- Missing Time of Inspection."
counter += 1
messages.append("🕒 Inspection #\(index + 1): Time of inspection is required")
}

if inspection.unknownPreviousWaterBody == true ||
inspection.commercialManufacturerAsPreviousWaterBody == true ||
inspection.previousDryStorage == true {
if inspection.previousMajorCities.isEmpty {
message = "\(message)\n\(counter)- Please add Closest Major City for Previous Waterbody."
counter += 1
}
// Previous Waterbody Validations
if (inspection.unknownPreviousWaterBody ||
inspection.commercialManufacturerAsPreviousWaterBody ||
inspection.previousDryStorage) {
messages.append("🌊 Inspection #\(index + 1): Previous waterbody requires closest major city")
}

if inspection.unknownDestinationWaterBody == true ||
inspection.commercialManufacturerAsDestinationWaterBody == true ||
inspection.destinationDryStorage == true {
if inspection.destinationMajorCities.isEmpty {
message = "\(message)\n\(counter)- Please add Closest Major City for Destination Waterbody."
counter += 1
}
// Destination Waterbody Validations
if (inspection.unknownDestinationWaterBody ||
inspection.commercialManufacturerAsDestinationWaterBody ||
inspection.destinationDryStorage) && inspection.destinationMajorCities.isEmpty {
messages.append("🎯 Inspection #\(index + 1): Destination waterbody requires closest major city")
}

if !inspection.highRiskAssessments.isEmpty {
for highRisk in inspection.highRiskAssessments {
if highRisk.sealIssued == true && highRisk.sealNumber <= 0 {
message = "\(message)\n\(counter)- Please input the Seal #."
counter += 1
}

if highRisk.decontaminationOrderIssued == true && highRisk.decontaminationOrderNumber <= 0 {
message = "\(message)\n\(counter)- Please input the Decontamination order number."
counter += 1
}
// High Risk Assessment Validations
for (riskIndex, highRisk) in inspection.highRiskAssessments.enumerated() {
if highRisk.sealIssued && highRisk.sealNumber <= 0 {
messages.append("🏷️ Inspection #\(index + 1) Risk #\(riskIndex + 1): Seal number is required")
}

if highRisk.decontaminationOrderIssued && highRisk.decontaminationOrderNumber <= 0 {
messages.append("📄 Inspection #\(index + 1) Risk #\(riskIndex + 1): Decontamination order number is required")
}
}
}

// Check for invalid inspections
// Form Validation Status
let invalidInspections = model.inspections.filter { !$0.formDidValidate }
if !invalidInspections.isEmpty {
message = "\(message)\n\(counter)- One or more inspections contain validation errors. Please review each inspection."
counter += 1
messages.append("❌ One or more inspections contain validation errors")
}

if !message.isEmpty {
model.set(status: .Errors)
}
if !messages.isEmpty {
model.set(status: .Errors)
}

return message
return messages.joined(separator: "\n\n")
}

func createTestModel() {
Expand Down
Loading

0 comments on commit 4bb2464

Please sign in to comment.