Commit 010b196a authored by Wei Han's avatar Wei Han

code update, sync ui

parent cda59edd
......@@ -11,7 +11,7 @@
44006126336D83DB220E7214 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; };
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
915A61EF3FB0D83A24210E3E /* libPods-example.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B30817B782A4551FE77ED4D /* libPods-example.a */; };
AF643AF9AD2E6D7F8427E3F1 /* libPods-example.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6FF98AE45D9936432AFEC029 /* libPods-example.a */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
......@@ -19,11 +19,11 @@
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = example/Images.xcassets; sourceTree = "<group>"; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = example/Info.plist; sourceTree = "<group>"; };
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = example/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
3CEA4A8FA353AAF71956ECAB /* Pods-example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-example.release.xcconfig"; path = "Target Support Files/Pods-example/Pods-example.release.xcconfig"; sourceTree = "<group>"; };
6224139EF36E532E27122179 /* Pods-example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-example.debug.xcconfig"; path = "Target Support Files/Pods-example/Pods-example.debug.xcconfig"; sourceTree = "<group>"; };
6B30817B782A4551FE77ED4D /* libPods-example.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-example.a"; sourceTree = BUILT_PRODUCTS_DIR; };
688F14F134731D6E3695DCD4 /* Pods-example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-example.release.xcconfig"; path = "Target Support Files/Pods-example/Pods-example.release.xcconfig"; sourceTree = "<group>"; };
6FF98AE45D9936432AFEC029 /* libPods-example.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-example.a"; sourceTree = BUILT_PRODUCTS_DIR; };
761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = example/AppDelegate.swift; sourceTree = "<group>"; };
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = example/LaunchScreen.storyboard; sourceTree = "<group>"; };
E7FE0B6CF3283929F0ED2217 /* Pods-example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-example.debug.xcconfig"; path = "Target Support Files/Pods-example/Pods-example.debug.xcconfig"; sourceTree = "<group>"; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */
......@@ -32,7 +32,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
915A61EF3FB0D83A24210E3E /* libPods-example.a in Frameworks */,
AF643AF9AD2E6D7F8427E3F1 /* libPods-example.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
......@@ -55,7 +55,7 @@
isa = PBXGroup;
children = (
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
6B30817B782A4551FE77ED4D /* libPods-example.a */,
6FF98AE45D9936432AFEC029 /* libPods-example.a */,
);
name = Frameworks;
sourceTree = "<group>";
......@@ -92,8 +92,8 @@
BBD78D7AC51CEA395F1C20DB /* Pods */ = {
isa = PBXGroup;
children = (
6224139EF36E532E27122179 /* Pods-example.debug.xcconfig */,
3CEA4A8FA353AAF71956ECAB /* Pods-example.release.xcconfig */,
E7FE0B6CF3283929F0ED2217 /* Pods-example.debug.xcconfig */,
688F14F134731D6E3695DCD4 /* Pods-example.release.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
......@@ -105,13 +105,13 @@
isa = PBXNativeTarget;
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "example" */;
buildPhases = (
AB27DA1BFBF250F3FBDD122A /* [CP] Check Pods Manifest.lock */,
73D6515E0B05E56E1C38DCEE /* [CP] Check Pods Manifest.lock */,
13B07F871A680F5B00A75B9A /* Sources */,
13B07F8C1A680F5B00A75B9A /* Frameworks */,
13B07F8E1A680F5B00A75B9A /* Resources */,
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
ECF4F483E82F128B69448B1F /* [CP] Embed Pods Frameworks */,
3BE667FD28A69559FE50BB89 /* [CP] Copy Pods Resources */,
13DCD710B66171E18819239B /* [CP] Embed Pods Frameworks */,
E355AFBEE78FE218C8B2F995 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
......@@ -183,24 +183,24 @@
shellPath = /bin/sh;
shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n";
};
3BE667FD28A69559FE50BB89 /* [CP] Copy Pods Resources */ = {
13DCD710B66171E18819239B /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-example/Pods-example-resources-${CONFIGURATION}-input-files.xcfilelist",
"${PODS_ROOT}/Target Support Files/Pods-example/Pods-example-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-example/Pods-example-resources-${CONFIGURATION}-output-files.xcfilelist",
"${PODS_ROOT}/Target Support Files/Pods-example/Pods-example-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-example/Pods-example-resources.sh\"\n";
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-example/Pods-example-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
AB27DA1BFBF250F3FBDD122A /* [CP] Check Pods Manifest.lock */ = {
73D6515E0B05E56E1C38DCEE /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
......@@ -222,21 +222,21 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
ECF4F483E82F128B69448B1F /* [CP] Embed Pods Frameworks */ = {
E355AFBEE78FE218C8B2F995 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-example/Pods-example-frameworks-${CONFIGURATION}-input-files.xcfilelist",
"${PODS_ROOT}/Target Support Files/Pods-example/Pods-example-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-example/Pods-example-frameworks-${CONFIGURATION}-output-files.xcfilelist",
"${PODS_ROOT}/Target Support Files/Pods-example/Pods-example-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-example/Pods-example-frameworks.sh\"\n";
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-example/Pods-example-resources.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
......@@ -255,7 +255,7 @@
/* Begin XCBuildConfiguration section */
13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 6224139EF36E532E27122179 /* Pods-example.debug.xcconfig */;
baseConfigurationReference = E7FE0B6CF3283929F0ED2217 /* Pods-example.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
......@@ -283,7 +283,7 @@
};
13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 3CEA4A8FA353AAF71956ECAB /* Pods-example.release.xcconfig */;
baseConfigurationReference = 688F14F134731D6E3695DCD4 /* Pods-example.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
......
......@@ -73,6 +73,8 @@
self.projectCode = @"PTD197656";
self.projectName = @"Ara E`Residence Dev";
[self showLoadingOverlay:@"Loading dashboard..."];
[self checkAndRefreshAPIs];
NSLog(@"in viewDidLoad ClientID: %@, ClientCode: %@, user_token: %@", self.ClientID, self.ClientCode, self.user_token);
NSLog(@"dashboard.drawerController = %@", self.drawerController);
......@@ -87,13 +89,15 @@
}
- (void)checkAndRefreshAPIs {
// Only trigger API calls when all props are set and not the placeholder
if (![_ClientID isEqualToString:@"clientID_loading"] &&
![_ClientCode isEqualToString:@"clientCode_loading"] &&
![_user_token isEqualToString:@"userToken_loading"]) {
NSLog(@"in checkAndRefreshApis ClientID: %@, ClientCode: %@, user_token: %@", self.ClientID, self.ClientCode, self.user_token);
[self refreshDashboardContent]; // Your API call method
if (![self isReadyToLoadDashboard]) {
NSLog(@"⏳ Waiting for ClientID / ClientCode / Token...");
return;
}
NSLog(@"✅ All credentials ready. Loading dashboard.");
[self showLoadingOverlay:@"Loading dashboard..."];
[self refreshDashboardContent];
}
#pragma mark - Refresh Logic
......@@ -119,11 +123,14 @@
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
if (dashboardData) [self processDashboardResponse:dashboardData];
if (announcementData) [self processAnnouncementResponse:announcementData];
// 👇 hide loading only when EVERYTHING is done
[self hideLoadingOverlay];
NSLog(@"✅ Dashboard refresh complete");
});
}
- (void)handlePullToRefresh:(UIRefreshControl *)refreshControl {
NSLog(@"🔄 Pull-to-refresh triggered");
......@@ -1249,6 +1256,17 @@
}
}
- (BOOL)isReadyToLoadDashboard {
return (
self.ClientID.length > 0 &&
self.ClientCode.length > 0 &&
self.user_token.length > 0 &&
![self.ClientID isEqualToString:@"clientID_loading"] &&
![self.ClientCode isEqualToString:@"clientCode_loading"] &&
![self.user_token isEqualToString:@"userToken_loading"]
);
}
#pragma mark - prop setter
- (void)setClientID:(NSString *)ClientID {
_ClientID = [ClientID copy];
......
// LocalStorage.h
#import <Foundation/Foundation.h>
@interface LocalStorage : NSObject
/// Generic load for any type
+ (id)loadObjectForKey:(NSString *)key;
/// Generic save for any object
+ (void)saveObject:(id)object forKey:(NSString *)key;
/// Remove object for a key
+ (void)removeObjectForKey:(NSString *)key;
/// Load array safely (for convenience)
+ (NSArray *)loadArrayForKey:(NSString *)key;
/// Save array safely (for convenience)
+ (void)saveArray:(NSArray *)array forKey:(NSString *)key;
@end
// LocalStorage.mm
#import "LocalStorage.h"
@implementation LocalStorage
+ (NSUserDefaults *)defaults {
return [NSUserDefaults standardUserDefaults];
}
#pragma mark - Generic Methods
+ (id)loadObjectForKey:(NSString *)key {
NSData *data = [[self defaults] objectForKey:key];
if (!data) return nil;
NSError *error = nil;
id object = [NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet setWithObjects:[NSArray class],[NSDictionary class],[NSString class],[NSNumber class], nil]
fromData:data
error:&error];
if (error) {
NSLog(@"Failed to unarchive object for key '%@': %@", key, error);
return nil;
}
return object;
}
+ (void)saveObject:(id)object forKey:(NSString *)key {
if (!object) return;
NSError *error = nil;
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:object
requiringSecureCoding:NO
error:&error];
if (error) {
NSLog(@"Failed to archive object for key '%@': %@", key, error);
return;
}
[[self defaults] setObject:data forKey:key];
}
+ (void)removeObjectForKey:(NSString *)key {
[[self defaults] removeObjectForKey:key];
}
#pragma mark - Convenience Array Methods
+ (NSArray *)loadArrayForKey:(NSString *)key {
id object = [self loadObjectForKey:key];
if ([object isKindOfClass:[NSArray class]]) {
return object;
}
return nil;
}
+ (void)saveArray:(NSArray *)array forKey:(NSString *)key {
[self saveObject:array forKey:key];
}
@end
//
// HistoryDetailsViewController.h
// QmsPluginFramework
//
// Created by Wei Han Ng on 19/12/2025.
//
//
// HistoryDetailsViewController.mm
// QmsPluginFramework
//
// Created by Wei Han Ng on 19/12/2025.
//
// PendingDetailsViewController.h
#import <UIKit/UIKit.h>
@interface PendingDetailsViewController : UIViewController
@property (nonatomic, strong) NSDictionary *item;
@end
// PendingDetailsViewController.mm
#import "PendingDetailsViewController.h"
@implementation PendingDetailsViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = UIColor.systemBackgroundColor;
UILabel *label = [[UILabel alloc] init];
label.translatesAutoresizingMaskIntoConstraints = NO;
label.numberOfLines = 0;
label.text = [NSString stringWithFormat:
@"Unit: %@\nBy: %@\nType: %@",
self.item[@"unitName"],
self.item[@"createdBy"],
self.item[@"type"]];
[self.view addSubview:label];
[NSLayoutConstraint activateConstraints:@[
[label.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
[label.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor],
]];
}
@end
......@@ -2,11 +2,129 @@
#import "SyncHistoryViewController.h"
typedef NS_ENUM(NSInteger, HistorySyncStatus) {
HistorySyncStatusSuccess,
HistorySyncStatusFailed
};
@interface HistorySyncItem : NSObject
@property (nonatomic, strong) NSString *unitName;
@property (nonatomic, strong) NSString *createdBy;
@property (nonatomic, strong) NSString *createdOn;
@property (nonatomic, strong) NSString *uploadedOn;
@property (nonatomic, strong) NSString *actionMade;
@property (nonatomic, assign) HistorySyncStatus status;
@end
@implementation HistorySyncItem
@end
@interface HistorySyncCell : UITableViewCell
- (void)configureWithItem:(HistorySyncItem *)item;
@end
@interface HistorySyncCell ()
@property (nonatomic, strong) UILabel *unitLabel;
@property (nonatomic, strong) UILabel *createdByLabel;
@property (nonatomic, strong) UILabel *createdOnLabel;
@property (nonatomic, strong) UILabel *uploadedOnLabel;
@property (nonatomic, strong) UILabel *actionLabel;
@end
@implementation HistorySyncCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style
reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (!self) return nil;
self.selectionStyle = UITableViewCellSelectionStyleNone;
self.backgroundColor = UIColor.clearColor; // 👈 important
self.contentView.backgroundColor = UIColor.whiteColor;
self.contentView.layer.cornerRadius = 10;
self.contentView.layer.masksToBounds = YES;
self.contentView.layer.borderWidth = 1.0;
self.contentView.layer.borderColor = [UIColor colorWithWhite:0.85 alpha:1.0].CGColor;
// 👇 spacing between cards
self.contentView.layoutMargins = UIEdgeInsetsMake(8, 12, 8, 12);
[self setupUI];
return self;
}
- (void)setupUI {
self.unitLabel = [self labelBold:16];
self.createdByLabel = [self labelRegular:13];
self.createdOnLabel = [self labelRegular:13];
self.uploadedOnLabel = [self labelRegular:13];
self.actionLabel = [self labelRegular:13];
UIStackView *stack = [[UIStackView alloc] initWithArrangedSubviews:@[
self.unitLabel,
self.createdByLabel,
self.createdOnLabel,
self.uploadedOnLabel,
self.actionLabel
]];
stack.axis = UILayoutConstraintAxisVertical;
stack.spacing = 4;
stack.translatesAutoresizingMaskIntoConstraints = NO;
[self.contentView addSubview:stack];
[NSLayoutConstraint activateConstraints:@[
[stack.topAnchor constraintEqualToAnchor:self.contentView.layoutMarginsGuide.topAnchor],
[stack.leadingAnchor constraintEqualToAnchor:self.contentView.layoutMarginsGuide.leadingAnchor],
[stack.trailingAnchor constraintEqualToAnchor:self.contentView.layoutMarginsGuide.trailingAnchor],
[stack.bottomAnchor constraintEqualToAnchor:self.contentView.layoutMarginsGuide.bottomAnchor],
]];
}
- (UILabel *)labelBold:(CGFloat)size {
UILabel *label = [[UILabel alloc] init];
label.font = [UIFont boldSystemFontOfSize:size];
label.textColor = UIColor.blackColor;
label.numberOfLines = 0;
return label;
}
- (UILabel *)labelRegular:(CGFloat)size {
UILabel *label = [[UILabel alloc] init];
label.font = [UIFont systemFontOfSize:size];
label.textColor = UIColor.darkGrayColor;
label.numberOfLines = 0;
return label;
}
- (void)configureWithItem:(HistorySyncItem *)item {
self.unitLabel.text = item.unitName;
self.createdByLabel.text = [NSString stringWithFormat:@"Created by: %@", item.createdBy];
self.createdOnLabel.text = [NSString stringWithFormat:@"Created on: %@", item.createdOn];
self.uploadedOnLabel.text = [NSString stringWithFormat:@"Uploaded on: %@", item.uploadedOn];
self.actionLabel.text = [NSString stringWithFormat:@"Action made: %@", item.actionMade];
}
@end
@interface SyncHistoryViewController () <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) UIView *headerView;
@property (nonatomic, strong) UIButton *backButton;
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UIButton *shareButton;
@property (nonatomic, strong) UIView *offlineBanner;
@property (nonatomic, strong) UIView *tabBarView;
@property (nonatomic, strong) UIButton *allTabButton;
......@@ -15,8 +133,13 @@
@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) UIView *emptyStateView;
@property (nonatomic, strong) UIButton *syncAgainButton;
@property (nonatomic, strong) NSArray<HistorySyncItem *> *allItems;
@property (nonatomic, strong) NSArray<HistorySyncItem *> *filteredItems;
@property (nonatomic, assign) NSInteger selectedTab;
@end
@implementation SyncHistoryViewController
......@@ -25,12 +148,17 @@
[super viewDidLoad];
self.view.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0];
self.selectedTab = 0;
[self setupHeader];
[self setupOfflineBanner];
[self setupTabBar];
[self setupTableView];
[self setupEmptyState];
[self setupBottomButton];
[self loadMockData];
[self applyFilter];
}
#pragma mark - Header
......@@ -43,11 +171,11 @@
self.backButton = [UIButton buttonWithType:UIButtonTypeCustom];
[self.backButton setImage:[UIImage systemImageNamed:@"chevron.backward"]
forState:UIControlStateNormal];
self.backButton.tintColor = UIColor.whiteColor;
[self.backButton addTarget:self
action:@selector(onBack)
forControlEvents:UIControlEventTouchUpInside];
self.backButton.translatesAutoresizingMaskIntoConstraints = NO;
self.backButton.tintColor = UIColor.whiteColor;
[self.headerView addSubview:self.backButton];
self.titleLabel = [[UILabel alloc] init];
......@@ -57,7 +185,19 @@
self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self.headerView addSubview:self.titleLabel];
// Constraints
self.shareButton = [UIButton buttonWithType:UIButtonTypeSystem];
[self.shareButton setTitle:@"Share to Email" forState:UIControlStateNormal];
[self.shareButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
self.shareButton.layer.borderColor = UIColor.whiteColor.CGColor;
self.shareButton.layer.borderWidth = 0.5;
self.shareButton.layer.cornerRadius = 14;
[self.shareButton addTarget:self
action:@selector(onShare)
forControlEvents:UIControlEventTouchUpInside];
self.shareButton.titleLabel.font = [UIFont systemFontOfSize:14];
self.shareButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.headerView addSubview:self.shareButton];
[NSLayoutConstraint activateConstraints:@[
[self.headerView.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor],
[self.headerView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
......@@ -71,6 +211,38 @@
[self.titleLabel.centerXAnchor constraintEqualToAnchor:self.headerView.centerXAnchor],
[self.titleLabel.centerYAnchor constraintEqualToAnchor:self.headerView.centerYAnchor],
[self.shareButton.trailingAnchor constraintEqualToAnchor:self.headerView.trailingAnchor constant:-8],
[self.shareButton.centerYAnchor constraintEqualToAnchor:self.headerView.centerYAnchor],
[self.shareButton.heightAnchor constraintEqualToConstant:28],
[self.shareButton.widthAnchor constraintEqualToConstant:120],
]];
}
#pragma mark - Setup Offline Banner
- (void)setupOfflineBanner {
self.offlineBanner = [[UIView alloc] init];
self.offlineBanner.backgroundColor = [UIColor colorWithRed:1.0 green:0.9 blue:0.6 alpha:1.0];
self.offlineBanner.translatesAutoresizingMaskIntoConstraints = NO;
self.offlineBanner.hidden = YES;
UILabel *label = [[UILabel alloc] init];
label.text = @"Offline Mode";
label.font = [UIFont boldSystemFontOfSize:14];
label.translatesAutoresizingMaskIntoConstraints = NO;
[self.offlineBanner addSubview:label];
[self.view addSubview:self.offlineBanner];
[NSLayoutConstraint activateConstraints:@[
[self.offlineBanner.topAnchor constraintEqualToAnchor:self.headerView.bottomAnchor],
[self.offlineBanner.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[self.offlineBanner.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[self.offlineBanner.heightAnchor constraintEqualToConstant:36],
[label.centerXAnchor constraintEqualToAnchor:self.offlineBanner.centerXAnchor],
[label.centerYAnchor constraintEqualToAnchor:self.offlineBanner.centerYAnchor],
]];
}
......@@ -81,9 +253,9 @@
self.tabBarView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.tabBarView];
self.allTabButton = [self createTabButton:@"All"];
self.successTabButton = [self createTabButton:@"Success"];
self.failedTabButton = [self createTabButton:@"Failed"];
self.allTabButton = [self createTabButton:@"All" tag:0];
self.successTabButton = [self createTabButton:@"Success" tag:1];
self.failedTabButton = [self createTabButton:@"Failed" tag:2];
UIStackView *stack = [[UIStackView alloc] initWithArrangedSubviews:@[
self.allTabButton,
......@@ -109,11 +281,13 @@
]];
}
- (UIButton *)createTabButton:(NSString *)title {
- (UIButton *)createTabButton:(NSString *)title tag:(NSInteger)tag {
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
btn.tag = tag;
btn.titleLabel.font = [UIFont systemFontOfSize:16];
[btn setTitle:title forState:UIControlStateNormal];
[btn setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
btn.titleLabel.font = [UIFont systemFontOfSize:16];
[btn addTarget:self action:@selector(onTabPressed:) forControlEvents:UIControlEventTouchUpInside];
return btn;
}
......@@ -123,13 +297,15 @@
self.tableView.translatesAutoresizingMaskIntoConstraints = NO;
self.tableView.delegate = self;
self.tableView.dataSource = self;
self.tableView.backgroundColor = UIColor.clearColor;
self.tableView.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0];
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
self.tableView.estimatedRowHeight = 120;
self.tableView.rowHeight = UITableViewAutomaticDimension;
[self.view addSubview:self.tableView];
[NSLayoutConstraint activateConstraints:@[
[self.tableView.topAnchor constraintEqualToAnchor:self.tabBarView.bottomAnchor],
[self.tableView.topAnchor constraintEqualToAnchor:self.tabBarView.bottomAnchor constant:30],
[self.tableView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[self.tableView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[self.tableView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:-60],
......@@ -138,28 +314,38 @@
// Dummy data
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 5; // placeholder
return self.filteredItems.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellId = @"HistoryCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellId];
static NSString *cellId = @"HistorySyncCell";
HistorySyncCell *cell =
[tableView dequeueReusableCellWithIdentifier:cellId];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
cell = [[HistorySyncCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:cellId];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
cell.textLabel.text = @"Unit A-01";
cell.detailTextLabel.text = @"Created on 12 August 2025\nUploaded on 14 August 2025";
cell.detailTextLabel.numberOfLines = 2;
HistorySyncItem *item = self.filteredItems[indexPath.row];
[cell configureWithItem:item];
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
UIViewController *detail = [UIViewController new];
detail.view.backgroundColor = UIColor.whiteColor;
detail.title = @"History Detail";
[self.navigationController pushViewController:detail animated:YES];
}
- (void)setupEmptyState {
self.emptyStateView = [[UIView alloc] init];
self.emptyStateView.translatesAutoresizingMaskIntoConstraints = NO;
......@@ -193,10 +379,10 @@
- (void)setupBottomButton {
self.syncAgainButton = [UIButton buttonWithType:UIButtonTypeSystem];
self.syncAgainButton.translatesAutoresizingMaskIntoConstraints = NO;
self.syncAgainButton.backgroundColor = self.headerView.backgroundColor;
self.syncAgainButton.backgroundColor = UIColor.lightGrayColor;
[self.syncAgainButton setTitle:@"SYNC AGAIN" forState:UIControlStateNormal];
[self.syncAgainButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
self.syncAgainButton.titleLabel.font = [UIFont boldSystemFontOfSize:16];
self.syncAgainButton.enabled = NO;
[self.view addSubview:self.syncAgainButton];
......@@ -213,4 +399,78 @@
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)onTabPressed:(UIButton *)sender {
self.selectedTab = sender.tag;
[self applyFilter];
[self updateTabUI];
}
- (void)updateTabUI {
NSArray *buttons = @[self.allTabButton, self.successTabButton, self.failedTabButton];
for (UIButton *btn in buttons) {
btn.alpha = (btn.tag == self.selectedTab) ? 1.0 : 0.6;
}
}
- (void)onShare {
UIAlertController *alert =
[UIAlertController alertControllerWithTitle:@"Share Pending Items"
message:@"Send pending items to email?"
preferredStyle:UIAlertControllerStyleActionSheet];
[alert addAction:[UIAlertAction actionWithTitle:@"Send"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * _Nonnull action) {
NSLog(@"Share triggered (stub)");
}]];
[alert addAction:[UIAlertAction actionWithTitle:@"Cancel"
style:UIAlertActionStyleCancel
handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
}
- (void)loadMockData {
HistorySyncItem *a = [HistorySyncItem new];
a.unitName = @"Unit A-01";
a.createdBy = @"John";
a.createdOn = @"12 Aug 2025";
a.uploadedOn = @"14 Aug 2025";
a.actionMade = @"Add Issue";
a.status = HistorySyncStatusSuccess;
HistorySyncItem *b = [HistorySyncItem new];
b.unitName = @"Unit B-02";
b.createdBy = @"Jane";
b.createdOn = @"10 Aug 2025";
b.uploadedOn = @"-";
b.actionMade = @"Add Appoinment";
b.status = HistorySyncStatusFailed;
self.allItems = @[a, b];
}
- (void)applyFilter {
if (self.selectedTab == 0) {
self.filteredItems = self.allItems;
} else {
HistorySyncStatus status =
(self.selectedTab == 1)
? HistorySyncStatusSuccess
: HistorySyncStatusFailed;
NSPredicate *predicate =
[NSPredicate predicateWithBlock:^BOOL(HistorySyncItem *item, NSDictionary *_) {
return item.status == status;
}];
self.filteredItems = [self.allItems filteredArrayUsingPredicate:predicate];
}
self.emptyStateView.hidden = (self.filteredItems.count > 0);
[self.tableView reloadData];
}
@end
// SyncPendingViewController.mm
#import "SyncPendingViewController.h"
#import "PendingDetailsViewController.h"
typedef NS_ENUM(NSInteger, SyncPendingViewMode) {
SyncPendingViewModeList,
SyncPendingViewModeDetails,
SyncPendingViewModeImage
};
@interface SyncPendingCardCell : UITableViewCell
- (void)configureWithItem:(NSDictionary *)item;
@end
@interface SyncPendingCardCell ()
@property (nonatomic, strong) UIView *cardView;
@property (nonatomic, strong) UILabel *unitLabel;
@property (nonatomic, strong) UILabel *createdByLabel;
@property (nonatomic, strong) UILabel *createdOnLabel;
@property (nonatomic, strong) UILabel *actionLabel;
@end
@implementation SyncPendingCardCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style
reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.backgroundColor = UIColor.clearColor;
self.selectionStyle = UITableViewCellSelectionStyleNone;
self.cardView = [[UIView alloc] init];
self.cardView.backgroundColor = UIColor.whiteColor;
self.cardView.layer.cornerRadius = 10;
self.cardView.layer.shadowColor = UIColor.blackColor.CGColor;
self.cardView.layer.shadowOpacity = 0.1;
self.cardView.layer.shadowRadius = 4;
self.cardView.layer.shadowOffset = CGSizeMake(0, 2);
self.cardView.translatesAutoresizingMaskIntoConstraints = NO;
[self.contentView addSubview:self.cardView];
self.unitLabel = [self makeLabel:16 bold:YES];
self.createdByLabel = [self makeLabel:13 bold:NO];
self.createdOnLabel = [self makeLabel:13 bold:NO];
self.actionLabel = [self makeLabel:13 bold:NO];
UIStackView *stack = [[UIStackView alloc] initWithArrangedSubviews:@[
self.unitLabel,
self.createdByLabel,
self.createdOnLabel,
self.actionLabel
]];
stack.axis = UILayoutConstraintAxisVertical;
stack.spacing = 4;
stack.translatesAutoresizingMaskIntoConstraints = NO;
[self.cardView addSubview:stack];
[NSLayoutConstraint activateConstraints:@[
[self.cardView.topAnchor constraintEqualToAnchor:self.contentView.topAnchor constant:8],
[self.cardView.bottomAnchor constraintEqualToAnchor:self.contentView.bottomAnchor constant:-8],
[self.cardView.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor constant:16],
[self.cardView.trailingAnchor constraintEqualToAnchor:self.contentView.trailingAnchor constant:-16],
[stack.topAnchor constraintEqualToAnchor:self.cardView.topAnchor constant:12],
[stack.bottomAnchor constraintEqualToAnchor:self.cardView.bottomAnchor constant:-12],
[stack.leadingAnchor constraintEqualToAnchor:self.cardView.leadingAnchor constant:12],
[stack.trailingAnchor constraintEqualToAnchor:self.cardView.trailingAnchor constant:-12],
]];
}
return self;
}
- (UILabel *)makeLabel:(CGFloat)size bold:(BOOL)bold {
UILabel *label = [[UILabel alloc] init];
label.font = bold ? [UIFont boldSystemFontOfSize:size]
: [UIFont systemFontOfSize:size];
label.textColor = UIColor.blackColor;
label.numberOfLines = 0;
return label;
}
- (void)configureWithItem:(NSDictionary *)item {
self.unitLabel.text = item[@"unitName"];
self.createdByLabel.text =
[NSString stringWithFormat:@"Created By: %@", item[@"createdBy"]];
self.createdOnLabel.text =
[NSString stringWithFormat:@"Created On: %@", item[@"createdOn"]];
self.actionLabel.text =
[NSString stringWithFormat:@"Action made: %@", item[@"type"]];
}
@end
@interface SyncPendingViewController () <UITableViewDelegate, UITableViewDataSource>
......@@ -14,13 +110,48 @@
// Placeholder data
@property (nonatomic, strong) NSArray *sections;
@property (nonatomic, assign) SyncPendingViewMode viewMode;
@property (nonatomic, strong) NSDictionary *selectedItem;
@property (nonatomic, assign) BOOL isLoading;
@end
@interface ProjectSectionHeaderView : UIView
@property (nonatomic, strong) UILabel *titleLabel;
@end
@implementation ProjectSectionHeaderView
- (instancetype)init {
self = [super init];
if (self) {
self.backgroundColor = UIColor.blackColor;
self.titleLabel = [[UILabel alloc] init];
self.titleLabel.textColor = UIColor.whiteColor;
self.titleLabel.font = [UIFont boldSystemFontOfSize:14];
self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:self.titleLabel];
[NSLayoutConstraint activateConstraints:@[
[self.titleLabel.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:16],
[self.titleLabel.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-16],
[self.titleLabel.centerYAnchor constraintEqualToAnchor:self.centerYAnchor],
]];
}
return self;
}
@end
@implementation SyncPendingViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.viewMode = SyncPendingViewModeList;
self.view.backgroundColor = [UIColor systemGroupedBackgroundColor];
[self setupHeader];
......@@ -53,11 +184,14 @@
[self.headerView addSubview:self.titleLabel];
self.shareButton = [UIButton buttonWithType:UIButtonTypeSystem];
[self.shareButton setTitle:@"Share" forState:UIControlStateNormal];
[self.shareButton setTitle:@"Share to Email" forState:UIControlStateNormal];
[self.shareButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
self.shareButton.layer.borderColor = UIColor.whiteColor.CGColor;
self.shareButton.layer.borderWidth = 0.5;
self.shareButton.layer.cornerRadius = 14;
[self.shareButton addTarget:self
action:@selector(onShare)
forControlEvents:UIControlEventTouchUpInside];
self.shareButton.titleLabel.font = [UIFont systemFontOfSize:14];
self.shareButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.headerView addSubview:self.shareButton];
......@@ -79,7 +213,7 @@
[self.shareButton.trailingAnchor constraintEqualToAnchor:self.headerView.trailingAnchor constant:-8],
[self.shareButton.centerYAnchor constraintEqualToAnchor:self.headerView.centerYAnchor],
[self.shareButton.heightAnchor constraintEqualToConstant:28],
[self.shareButton.widthAnchor constraintEqualToConstant:90],
[self.shareButton.widthAnchor constraintEqualToConstant:120],
]];
}
......@@ -91,9 +225,10 @@
self.tableView.delegate = self;
self.tableView.dataSource = self;
self.tableView.backgroundColor = UIColor.clearColor;
self.tableView.estimatedRowHeight = 120;
[self.tableView registerClass:UITableViewCell.class
forCellReuseIdentifier:@"Cell"];
[self.tableView registerClass:SyncPendingCardCell.class
forCellReuseIdentifier:@"CardCell"];
[self.view addSubview:self.tableView];
......@@ -109,12 +244,32 @@
- (void)setupDummyData {
self.sections = @[
@{
@"title": @"Project Alpha",
@"items": @[@"Item 1", @"Item 2"]
@"project": @"Project Alpha",
@"items": @[
@{
@"unitName": @"Unit A-01",
@"createdBy": @"John",
@"createdOn": @"2024-12-01",
@"type": @"Inspection"
},
@{
@"title": @"Project Beta",
@"items": @[@"Item A"]
@"unitName": @"Unit A-02",
@"createdBy": @"Sarah",
@"createdOn": @"2024-12-02",
@"type": @"Checklist"
}
]
},
@{
@"project": @"Project Beta",
@"items": @[
@{
@"unitName": @"Unit B-01",
@"createdBy": @"Mike",
@"createdOn": @"2024-11-20",
@"type": @"Maintenance"
}
]
}
];
}
......@@ -129,31 +284,104 @@
return [self.sections[section][@"items"] count];
}
- (NSString *)tableView:(UITableView *)tableView
titleForHeaderInSection:(NSInteger)section {
return self.sections[section][@"title"];
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell =
[tableView dequeueReusableCellWithIdentifier:@"Cell"
SyncPendingCardCell *cell =
[tableView dequeueReusableCellWithIdentifier:@"CardCell"
forIndexPath:indexPath];
cell.textLabel.text =
NSDictionary *item =
self.sections[indexPath.section][@"items"][indexPath.row];
cell.backgroundColor = UIColor.whiteColor;
cell.layer.cornerRadius = 10;
cell.layer.masksToBounds = YES;
[cell configureWithItem:item];
return cell;
}
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSDictionary *item =
self.sections[indexPath.section][@"items"][indexPath.row];
self.selectedItem = item;
self.viewMode = SyncPendingViewModeDetails;
[self showDetailsForItem:item];
}
- (CGFloat)tableView:(UITableView *)tableView
heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
- (UIView *)tableView:(UITableView *)tableView
viewForHeaderInSection:(NSInteger)section {
ProjectSectionHeaderView *header =
[[ProjectSectionHeaderView alloc] init];
header.titleLabel.text =
self.sections[section][@"project"];
return header;
}
- (CGFloat)tableView:(UITableView *)tableView
heightForHeaderInSection:(NSInteger)section {
return 36; // matches OG nicely
}
#pragma mark - Helpers
- (void)onBack {
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)showDetailsForItem:(NSDictionary *)item {
PendingDetailsViewController *vc = [PendingDetailsViewController new];
vc.item = item;
[self.navigationController pushViewController:vc animated:YES];
}
- (void)onShare {
UIAlertController *alert =
[UIAlertController alertControllerWithTitle:@"Share Pending Items"
message:@"Send pending items to email?"
preferredStyle:UIAlertControllerStyleActionSheet];
[alert addAction:[UIAlertAction actionWithTitle:@"Send"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * _Nonnull action) {
NSLog(@"Share triggered (stub)");
}]];
[alert addAction:[UIAlertAction actionWithTitle:@"Cancel"
style:UIAlertActionStyleCancel
handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
}
- (UIView *)emptyStateView {
UILabel *label = [[UILabel alloc] init];
label.text = @"No pending items";
label.textColor = UIColor.secondaryLabelColor;
label.textAlignment = NSTextAlignmentCenter;
return label;
}
- (void)reloadUI {
if (self.sections.count == 0) {
self.tableView.backgroundView = [self emptyStateView];
} else {
self.tableView.backgroundView = nil;
}
[self.tableView reloadData];
}
- (void)setIsLoading:(BOOL)isLoading {
_isLoading = isLoading;
self.view.userInteractionEnabled = !isLoading;
}
@end
......@@ -4,12 +4,6 @@
@interface SyncProjectViewController : UIViewController
typedef NS_ENUM(NSInteger, SyncProjectScreenState) {
SyncProjectScreenStateOfflineUnit,
SyncProjectScreenStateSelectProject,
SyncProjectScreenStateSelectUnit
};
@end
// SyncProjectViewController.mm
#import "SyncProjectViewController.h"
#import "LocalStorage.h"
typedef NS_ENUM(NSInteger, SyncProjectScreenState) {
SyncProjectScreenStateOfflineUnit,
SyncProjectScreenStateSelectProject,
SyncProjectScreenStateSelectUnit
};
static NSString * const kSyncSelectedUnitsKey = @"SYNC_SELECTEDUNITS";
static NSString * const kProjectListKey = @"PROJECT_LIST";
@interface SyncUnit : NSObject
@property (nonatomic, strong) NSString *planId;
@property (nonatomic, strong) NSString *displayName;
@property (nonatomic, strong) NSString *projectId;
@property (nonatomic, strong) NSString *projectName;
@property (nonatomic, assign) BOOL selected;
@end
@implementation SyncUnit
@end
@interface SyncProject : NSObject
@property (nonatomic, strong) NSString *projectId;
@property (nonatomic, strong) NSString *projectName;
@property (nonatomic, strong) NSArray<SyncUnit *> *units;
@end
@implementation SyncProject
@end
@interface SyncProjectViewController ()
<
UITableViewDelegate,
UITableViewDataSource,
UICollectionViewDelegate,
UICollectionViewDataSource
>
// Header
@property (nonatomic, strong) UIView *headerView;
......@@ -22,16 +57,28 @@
@property (nonatomic, assign) NSInteger selectedUnitCount;
@property (nonatomic, assign) SyncProjectScreenState screenState;
@property (nonatomic, assign) BOOL hasChanges;
@property (nonatomic, strong) NSMutableArray<SyncUnit *> *selectedUnits;
@property (nonatomic, strong) NSMutableArray<SyncProject *> *dummyProjects;
@property (nonatomic, strong) NSMutableArray<SyncUnit *> *dummyUnits;
@property (nonatomic, strong) UITableView *offlineTableView;
@property (nonatomic, strong) UITableView *unitTableView;
@property (nonatomic, strong) UICollectionView *projectCollectionView;
@end
@implementation SyncProjectViewController
@implementation SyncProjectViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor systemGroupedBackgroundColor];
self.selectedUnitCount = 0;
self.selectedUnits = [NSMutableArray array];
[self loadSelectedUnits];
[self setupDummyData];
[self setupHeader];
[self setupContentContainers];
......@@ -40,6 +87,7 @@
[self setupSelectUnitView];
[self showOfflineUnit];
[self setScreenState:SyncProjectScreenStateOfflineUnit];
}
#pragma mark - Header
......@@ -48,17 +96,12 @@
self.headerView.backgroundColor = [UIColor colorWithRed:0.0 green:0.5 blue:0.6 alpha:1.0];
self.headerView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.headerView];
self.backButton = [UIButton buttonWithType:UIButtonTypeCustom];
[self.backButton setImage:[UIImage systemImageNamed:@"chevron.backward"]
forState:UIControlStateNormal];
[self.backButton addTarget:self
action:@selector(onBack)
forControlEvents:UIControlEventTouchUpInside];
[self.backButton setImage:[UIImage systemImageNamed:@"chevron.backward"] forState:UIControlStateNormal];
[self.backButton addTarget:self action:@selector(onBack) forControlEvents:UIControlEventTouchUpInside];
self.backButton.translatesAutoresizingMaskIntoConstraints = NO;
self.backButton.tintColor = UIColor.whiteColor;
[self.headerView addSubview:self.backButton];
self.titleLabel = [[UILabel alloc] init];
self.titleLabel.text = @"Offline Unit";
self.titleLabel.textColor = UIColor.whiteColor;
......@@ -72,12 +115,10 @@
[self.headerView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[self.headerView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[self.headerView.heightAnchor constraintEqualToConstant:48],
[self.backButton.leadingAnchor constraintEqualToAnchor:self.headerView.leadingAnchor constant:8],
[self.backButton.centerYAnchor constraintEqualToAnchor:self.headerView.centerYAnchor],
[self.backButton.widthAnchor constraintEqualToConstant:32],
[self.backButton.heightAnchor constraintEqualToConstant:32],
[self.titleLabel.centerXAnchor constraintEqualToAnchor:self.headerView.centerXAnchor],
[self.titleLabel.centerYAnchor constraintEqualToAnchor:self.headerView.centerYAnchor],
]];
......@@ -87,7 +128,7 @@
self.titleLabel.text = title;
}
#pragma mark - Setup Containers
#pragma mark - Setup ContentContainer
- (void)setupContentContainers {
self.offlineUnitView = [UIView new];
self.selectProjectView = [UIView new];
......@@ -109,7 +150,7 @@
}
}
#pragma mark - Setup Offline View
#pragma mark - Offline Unit View
- (void)setupOfflineUnitView {
UILabel *countLabel = [UILabel new];
......@@ -131,14 +172,20 @@
infoLabel.font = [UIFont systemFontOfSize:16];
infoLabel.translatesAutoresizingMaskIntoConstraints = NO;
UITableView *tableView = [[UITableView alloc] initWithFrame:CGRectZero
self.offlineTableView =
[[UITableView alloc] initWithFrame:CGRectZero
style:UITableViewStyleGrouped];
tableView.translatesAutoresizingMaskIntoConstraints = NO;
self.offlineTableView.translatesAutoresizingMaskIntoConstraints = NO;
self.offlineTableView.delegate = self;
self.offlineTableView.dataSource = self;
[self.offlineTableView registerClass:UITableViewCell.class
forCellReuseIdentifier:@"OfflineUnitCell"];
[self.offlineUnitView addSubview:countLabel];
[self.offlineUnitView addSubview:addButton];
[self.offlineUnitView addSubview:infoLabel];
[self.offlineUnitView addSubview:tableView];
[self.offlineUnitView addSubview:self.offlineTableView];
[NSLayoutConstraint activateConstraints:@[
[countLabel.topAnchor constraintEqualToAnchor:self.offlineUnitView.topAnchor constant:16],
......@@ -152,14 +199,14 @@
[infoLabel.topAnchor constraintEqualToAnchor:countLabel.bottomAnchor constant:12],
[infoLabel.leadingAnchor constraintEqualToAnchor:countLabel.leadingAnchor],
[tableView.topAnchor constraintEqualToAnchor:infoLabel.bottomAnchor constant:12],
[tableView.leadingAnchor constraintEqualToAnchor:self.offlineUnitView.leadingAnchor],
[tableView.trailingAnchor constraintEqualToAnchor:self.offlineUnitView.trailingAnchor],
[tableView.bottomAnchor constraintEqualToAnchor:self.offlineUnitView.bottomAnchor],
[self.offlineTableView.topAnchor constraintEqualToAnchor:infoLabel.bottomAnchor constant:12],
[self.offlineTableView.leadingAnchor constraintEqualToAnchor:self.offlineUnitView.leadingAnchor],
[self.offlineTableView.trailingAnchor constraintEqualToAnchor:self.offlineUnitView.trailingAnchor],
[self.offlineTableView.bottomAnchor constraintEqualToAnchor:self.offlineUnitView.bottomAnchor],
]];
}
#pragma mark - Setup Project View
#pragma mark - Select Project View
- (void)setupSelectProjectView {
UILabel *countLabel = [UILabel new];
......@@ -175,17 +222,23 @@
searchBar.translatesAutoresizingMaskIntoConstraints = NO;
UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new];
layout.itemSize = CGSizeMake(self.view.bounds.size.width - 32, 150);
UICollectionView *collectionView =
[[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
collectionView.translatesAutoresizingMaskIntoConstraints = NO;
collectionView.backgroundColor = UIColor.clearColor;
layout.itemSize = CGSizeMake(self.view.bounds.size.width - 32, 120);
layout.minimumLineSpacing = 12;
self.projectCollectionView =
[[UICollectionView alloc] initWithFrame:CGRectZero
collectionViewLayout:layout];
self.projectCollectionView.translatesAutoresizingMaskIntoConstraints = NO;
self.projectCollectionView.backgroundColor = UIColor.clearColor;
self.projectCollectionView.delegate = self;
self.projectCollectionView.dataSource = self;
[self.projectCollectionView registerClass:UICollectionViewCell.class
forCellWithReuseIdentifier:@"ProjectCell"];
[self.selectProjectView addSubview:countLabel];
[self.selectProjectView addSubview:instructionLabel];
[self.selectProjectView addSubview:searchBar];
[self.selectProjectView addSubview:collectionView];
[self.selectProjectView addSubview:self.projectCollectionView];
[NSLayoutConstraint activateConstraints:@[
[countLabel.topAnchor constraintEqualToAnchor:self.selectProjectView.topAnchor constant:16],
......@@ -198,14 +251,14 @@
[searchBar.leadingAnchor constraintEqualToAnchor:self.selectProjectView.leadingAnchor],
[searchBar.trailingAnchor constraintEqualToAnchor:self.selectProjectView.trailingAnchor],
[collectionView.topAnchor constraintEqualToAnchor:searchBar.bottomAnchor constant:8],
[collectionView.leadingAnchor constraintEqualToAnchor:self.selectProjectView.leadingAnchor],
[collectionView.trailingAnchor constraintEqualToAnchor:self.selectProjectView.trailingAnchor],
[collectionView.bottomAnchor constraintEqualToAnchor:self.selectProjectView.bottomAnchor],
[self.projectCollectionView.topAnchor constraintEqualToAnchor:searchBar.bottomAnchor constant:8],
[self.projectCollectionView.leadingAnchor constraintEqualToAnchor:self.selectProjectView.leadingAnchor],
[self.projectCollectionView.trailingAnchor constraintEqualToAnchor:self.selectProjectView.trailingAnchor],
[self.projectCollectionView.bottomAnchor constraintEqualToAnchor:self.selectProjectView.bottomAnchor],
]];
}
#pragma mark - Setup Select View
#pragma mark - Select Unit View
- (void)setupSelectUnitView {
UILabel *countLabel = [UILabel new];
......@@ -219,8 +272,12 @@
UISearchBar *searchBar = [UISearchBar new];
searchBar.translatesAutoresizingMaskIntoConstraints = NO;
UITableView *tableView = [[UITableView alloc] init];
tableView.translatesAutoresizingMaskIntoConstraints = NO;
self.unitTableView = [[UITableView alloc] init];
self.unitTableView.translatesAutoresizingMaskIntoConstraints = NO;
self.unitTableView.delegate = self;
self.unitTableView.dataSource = self;
[self.unitTableView registerClass:UITableViewCell.class
forCellReuseIdentifier:@"UnitCell"];
UIButton *doneButton = [UIButton buttonWithType:UIButtonTypeSystem];
[doneButton setTitle:@"DONE" forState:UIControlStateNormal];
......@@ -235,7 +292,7 @@
[self.selectUnitView addSubview:countLabel];
[self.selectUnitView addSubview:instructionLabel];
[self.selectUnitView addSubview:searchBar];
[self.selectUnitView addSubview:tableView];
[self.selectUnitView addSubview:self.unitTableView];
[self.selectUnitView addSubview:doneButton];
[NSLayoutConstraint activateConstraints:@[
......@@ -249,11 +306,11 @@
[searchBar.leadingAnchor constraintEqualToAnchor:self.selectUnitView.leadingAnchor],
[searchBar.trailingAnchor constraintEqualToAnchor:self.selectUnitView.trailingAnchor],
[tableView.topAnchor constraintEqualToAnchor:searchBar.bottomAnchor constant:8],
[tableView.leadingAnchor constraintEqualToAnchor:self.selectUnitView.leadingAnchor],
[tableView.trailingAnchor constraintEqualToAnchor:self.selectUnitView.trailingAnchor],
[self.unitTableView.topAnchor constraintEqualToAnchor:searchBar.bottomAnchor constant:8],
[self.unitTableView.leadingAnchor constraintEqualToAnchor:self.selectUnitView.leadingAnchor],
[self.unitTableView.trailingAnchor constraintEqualToAnchor:self.selectUnitView.trailingAnchor],
[doneButton.topAnchor constraintEqualToAnchor:tableView.bottomAnchor constant:12],
[doneButton.topAnchor constraintEqualToAnchor:self.unitTableView.bottomAnchor constant:12],
[doneButton.leadingAnchor constraintEqualToAnchor:self.selectUnitView.leadingAnchor constant:16],
[doneButton.trailingAnchor constraintEqualToAnchor:self.selectUnitView.trailingAnchor constant:-16],
[doneButton.heightAnchor constraintEqualToConstant:48],
......@@ -261,9 +318,128 @@
]];
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (tableView == self.offlineTableView) {
return self.dummyUnits.count;
}
if (tableView == self.unitTableView) {
return self.dummyUnits.count;
}
return 0;
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell;
if (tableView == self.offlineTableView) {
cell = [tableView dequeueReusableCellWithIdentifier:@"OfflineUnitCell"
forIndexPath:indexPath];
} else {
cell = [tableView dequeueReusableCellWithIdentifier:@"UnitCell"
forIndexPath:indexPath];
}
SyncUnit *unit = self.dummyUnits[indexPath.row];
cell.textLabel.text = unit.displayName;
cell.accessoryType = unit.selected
? UITableViewCellAccessoryCheckmark
: UITableViewCellAccessoryNone;
return cell;
}
#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
SyncUnit *unit = self.dummyUnits[indexPath.row];
[self toggleUnit:unit];
[tableView reloadRowsAtIndexPaths:@[indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
}
#pragma mark - UICollectionViewDataSource
- (NSInteger)collectionView:(UICollectionView *)collectionView
numberOfItemsInSection:(NSInteger)section {
return self.dummyProjects.count;
}
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell =
[collectionView dequeueReusableCellWithReuseIdentifier:@"ProjectCell"
forIndexPath:indexPath];
cell.contentView.backgroundColor = UIColor.systemGray6Color;
cell.layer.cornerRadius = 12;
UILabel *label = [cell.contentView viewWithTag:100];
if (!label) {
label = [[UILabel alloc] initWithFrame:cell.contentView.bounds];
label.tag = 100;
label.font = [UIFont boldSystemFontOfSize:16];
label.textAlignment = NSTextAlignmentCenter;
[cell.contentView addSubview:label];
}
SyncProject *project = self.dummyProjects[indexPath.item];
label.text = project.projectName;
return cell;
}
#pragma mark - Screen State
- (void)setScreenState:(SyncProjectScreenState)state {
_screenState = state;
self.offlineUnitView.hidden = YES;
self.selectProjectView.hidden = YES;
self.selectUnitView.hidden = YES;
switch (state) {
case SyncProjectScreenStateOfflineUnit:
self.offlineUnitView.hidden = NO;
[self.offlineTableView reloadData];
break;
case SyncProjectScreenStateSelectProject:
self.selectProjectView.hidden = NO;
[self.projectCollectionView reloadData];
break;
case SyncProjectScreenStateSelectUnit:
self.selectUnitView.hidden = NO;
[self.unitTableView reloadData];
break;
}
}
#pragma mark - Helpers
- (void)onBack {
switch (self.screenState) {
case SyncProjectScreenStateOfflineUnit:
[self dismissViewControllerAnimated:YES completion:nil];
break;
case SyncProjectScreenStateSelectProject:
[self setScreenState:SyncProjectScreenStateOfflineUnit];
break;
case SyncProjectScreenStateSelectUnit:
[self setScreenState:SyncProjectScreenStateSelectProject];
break;
}
}
- (void)hideAllContent {
......@@ -298,4 +474,118 @@
[self showOfflineUnit];
}
- (BOOL)canAddUnit {
return self.selectedUnits.count < 10;
}
- (void)toggleUnit:(SyncUnit *)unit {
if (unit.selected) {
unit.selected = NO;
[self.selectedUnits removeObject:unit];
} else {
if (![self canAddUnit]) {
NSLog(@"only 10 units recommended");
return;
}
unit.selected = YES;
[self.selectedUnits addObject:unit];
}
self.hasChanges = YES;
self.selectedUnitCount = self.selectedUnits.count;
[self saveSelectedUnits];
}
- (void)showLoading:(BOOL)show {
if (show) {
[self.loadingView startAnimating];
} else {
[self.loadingView stopAnimating];
}
}
- (NSDictionary *)dictionaryFromUnit:(SyncUnit *)unit {
return @{
@"plan_id": unit.planId ?: @"",
@"sort_name": unit.displayName ?: @"",
@"project": @{
@"id": unit.projectId ?: @"",
@"project_name": unit.projectName ?: @""
}
};
}
- (SyncUnit *)unitFromDictionary:(NSDictionary *)dict {
SyncUnit *unit = [SyncUnit new];
unit.planId = dict[@"plan_id"];
unit.displayName = dict[@"sort_name"];
NSDictionary *project = dict[@"project"];
unit.projectId = project[@"id"];
unit.projectName = project[@"project_name"];
unit.selected = YES;
return unit;
}
- (void)saveSelectedUnits {
NSMutableArray *array = [NSMutableArray array];
for (SyncUnit *unit in self.selectedUnits) {
[array addObject:[self dictionaryFromUnit:unit]];
}
[LocalStorage saveArray:array forKey:kSyncSelectedUnitsKey];
}
- (void)loadSelectedUnits {
NSArray *stored = [LocalStorage loadArrayForKey:kSyncSelectedUnitsKey];
if (!stored) return;
[self.selectedUnits removeAllObjects];
for (NSDictionary *dict in stored) {
SyncUnit *unit = [self unitFromDictionary:dict];
[self.selectedUnits addObject:unit];
}
self.selectedUnitCount = self.selectedUnits.count;
}
- (void)setupDummyData {
// Dummy Units
self.dummyUnits = [NSMutableArray array];
for (int i = 1; i <= 12; i++) {
SyncUnit *unit = [SyncUnit new];
unit.planId = [NSString stringWithFormat:@"P%02d", i];
unit.displayName = [NSString stringWithFormat:@"Unit %d-A", i];
unit.projectId = [NSString stringWithFormat:@"PRJ%02d", i];
unit.projectName = [NSString stringWithFormat:@"Project %d", i];
unit.selected = i <= 3; // preselect first 3 units
[self.dummyUnits addObject:unit];
}
// Dummy Projects
self.dummyProjects = [NSMutableArray array];
for (int i = 1; i <= 5; i++) {
SyncProject *project = [SyncProject new];
project.projectId = [NSString stringWithFormat:@"PRJ%02d", i];
project.projectName = [NSString stringWithFormat:@"Project %d", i];
NSMutableArray *units = [NSMutableArray array];
for (int j = 1; j <= 3; j++) {
SyncUnit *unit = [SyncUnit new];
unit.planId = [NSString stringWithFormat:@"PRJ%02d-U%d", i, j];
unit.displayName = [NSString stringWithFormat:@"Unit %d-%d", i, j];
unit.projectId = project.projectId;
unit.projectName = project.projectName;
unit.selected = NO;
[units addObject:unit];
}
project.units = units;
[self.dummyProjects addObject:project];
}
}
@end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment