Commit 82cef0dc authored by Wei Han's avatar Wei Han

code update

parent 4a7abd98
...@@ -239,6 +239,7 @@ ...@@ -239,6 +239,7 @@
TargetAttributes = { TargetAttributes = {
2F9647642E86307D002CC7CB = { 2F9647642E86307D002CC7CB = {
CreatedOnToolsVersion = 26.0.1; CreatedOnToolsVersion = 26.0.1;
LastSwiftMigration = 2620;
}; };
2FC779A82F0E1C310002A1D4 = { 2FC779A82F0E1C310002A1D4 = {
CreatedOnToolsVersion = 26.2; CreatedOnToolsVersion = 26.2;
...@@ -312,6 +313,7 @@ ...@@ -312,6 +313,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
BUILD_LIBRARY_FOR_DISTRIBUTION = YES; BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
...@@ -343,6 +345,7 @@ ...@@ -343,6 +345,7 @@
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 4.0; SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
}; };
...@@ -352,6 +355,7 @@ ...@@ -352,6 +355,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
BUILD_LIBRARY_FOR_DISTRIBUTION = YES; BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
......
...@@ -514,7 +514,7 @@ ...@@ -514,7 +514,7 @@
[APIClient requestCancelAppointment:self.appointmentData[@"appointment_id"] [APIClient requestCancelAppointment:self.appointmentData[@"appointment_id"]
reason:remarks reason:remarks
completion:^(NSDictionary *data, NSError *error) completion:^(BOOL success, NSDictionary *data, NSError *error)
{ {
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
......
...@@ -933,7 +933,7 @@ ...@@ -933,7 +933,7 @@
[APIClient requestCancelAppointment:self.appointmentData[@"id"] [APIClient requestCancelAppointment:self.appointmentData[@"id"]
reason:remarks reason:remarks
completion:^(NSDictionary *data, NSError *error) completion:^(BOOL success, NSDictionary *data, NSError *error)
{ {
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
...@@ -966,7 +966,7 @@ ...@@ -966,7 +966,7 @@
} }
-(void) requestGeneralInfo { -(void) requestGeneralInfo {
[APIClient requestGeneralInfo:^(NSDictionary *data, NSError *error) { [APIClient requestGeneralInfo:^(BOOL success, NSDictionary *data, NSError *error) {
if (error) return; if (error) return;
NSDictionary *generalInfo = data[@"Data"]; NSDictionary *generalInfo = data[@"Data"];
self.floorPlanImage = generalInfo[@"image"]; self.floorPlanImage = generalInfo[@"image"];
......
...@@ -32,6 +32,7 @@ ...@@ -32,6 +32,7 @@
@property (nonatomic, strong) WKWebView *termsWebView; @property (nonatomic, strong) WKWebView *termsWebView;
@property (nonatomic, strong) NSString *termsAndCondition; @property (nonatomic, strong) NSString *termsAndCondition;
@property (nonatomic, strong) NSString *appointmentType; @property (nonatomic, strong) NSString *appointmentType;
@property (nonatomic, assign) BOOL hasLoadedTermsHTML;
// Labels // Labels
@property (nonatomic, strong) UILabel *unitLabel; @property (nonatomic, strong) UILabel *unitLabel;
...@@ -61,12 +62,23 @@ ...@@ -61,12 +62,23 @@
- (void)viewDidLoad { - (void)viewDidLoad {
[super viewDidLoad]; [super viewDidLoad];
self.view.backgroundColor = UIColor.whiteColor; self.view.backgroundColor = UIColor.whiteColor;
[self requestAppointment];
[self requestGeneralInfo];
[self setupHeader]; [self setupHeader];
[self setupFooter]; [self setupFooter];
[self setupUI]; [self setupUI];
} }
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
if (self.termsAndCondition && !self.hasLoadedTermsHTML) {
[self.termsWebView loadHTMLString:self.termsAndCondition baseURL:nil];
self.hasLoadedTermsHTML = YES;
}
}
#pragma mark - Setup UI #pragma mark - Setup UI
- (void)setupUI { - (void)setupUI {
// Scroll view // Scroll view
...@@ -173,26 +185,30 @@ ...@@ -173,26 +185,30 @@
#pragma mark = Overlay t&c #pragma mark = Overlay t&c
- (void)setupTermsOverlay { - (void)setupTermsOverlay {
// Overlay background
self.termsOverlay = [[UIView alloc] init]; self.termsOverlay = [[UIView alloc] init];
self.termsOverlay.backgroundColor = UIColor.whiteColor; self.termsOverlay.backgroundColor = UIColor.whiteColor;
self.termsOverlay.translatesAutoresizingMaskIntoConstraints = NO; self.termsOverlay.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.termsOverlay]; [self.view addSubview:self.termsOverlay];
UILayoutGuide *safe = self.view.safeAreaLayoutGuide;
[NSLayoutConstraint activateConstraints:@[ [NSLayoutConstraint activateConstraints:@[
[self.termsOverlay.topAnchor constraintEqualToAnchor:self.headerBar.bottomAnchor], [self.termsOverlay.topAnchor constraintEqualToAnchor:self.headerBar.bottomAnchor],
[self.termsOverlay.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], [self.termsOverlay.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[self.termsOverlay.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], [self.termsOverlay.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[self.termsOverlay.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor], [self.termsOverlay.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor]
]]; ]];
// Title
self.termsTitleLabel = [[UILabel alloc] init]; self.termsTitleLabel = [[UILabel alloc] init];
self.termsTitleLabel.text = @"Terms & Conditions"; self.termsTitleLabel.text = @"Terms & Conditions";
self.termsTitleLabel.font = [UIFont boldSystemFontOfSize:18]; self.termsTitleLabel.font = [UIFont boldSystemFontOfSize:18];
self.termsTitleLabel.translatesAutoresizingMaskIntoConstraints = NO; self.termsTitleLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self.termsOverlay addSubview:self.termsTitleLabel]; [self.termsOverlay addSubview:self.termsTitleLabel];
// WebView container
self.termsContainer = [[UIView alloc] init]; self.termsContainer = [[UIView alloc] init];
self.termsContainer.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0]; self.termsContainer.backgroundColor = [UIColor clearColor];
self.termsContainer.layer.cornerRadius = 12.0; self.termsContainer.layer.cornerRadius = 12.0;
self.termsContainer.translatesAutoresizingMaskIntoConstraints = NO; self.termsContainer.translatesAutoresizingMaskIntoConstraints = NO;
[self.termsOverlay addSubview:self.termsContainer]; [self.termsOverlay addSubview:self.termsContainer];
...@@ -202,12 +218,10 @@ ...@@ -202,12 +218,10 @@
self.termsWebView.translatesAutoresizingMaskIntoConstraints = NO; self.termsWebView.translatesAutoresizingMaskIntoConstraints = NO;
self.termsWebView.scrollView.delegate = self; self.termsWebView.scrollView.delegate = self;
self.termsWebView.navigationDelegate = self; self.termsWebView.navigationDelegate = self;
self.termsWebView.opaque = NO;
self.termsWebView.backgroundColor = [UIColor clearColor];
[self.termsContainer addSubview:self.termsWebView]; [self.termsContainer addSubview:self.termsWebView];
self.termsContainer.backgroundColor = [UIColor yellowColor];
self.termsWebView.backgroundColor = [UIColor redColor];
// Agree button // Agree button
self.agreeButton = [UIButton buttonWithType:UIButtonTypeSystem]; self.agreeButton = [UIButton buttonWithType:UIButtonTypeSystem];
self.agreeButton.translatesAutoresizingMaskIntoConstraints = NO; self.agreeButton.translatesAutoresizingMaskIntoConstraints = NO;
...@@ -220,7 +234,7 @@ ...@@ -220,7 +234,7 @@
forControlEvents:UIControlEventTouchUpInside]; forControlEvents:UIControlEventTouchUpInside];
[self.termsOverlay addSubview:self.agreeButton]; [self.termsOverlay addSubview:self.agreeButton];
// Layout // Layout constraints
[NSLayoutConstraint activateConstraints:@[ [NSLayoutConstraint activateConstraints:@[
[self.termsTitleLabel.topAnchor constraintEqualToAnchor:self.termsOverlay.topAnchor constant:40], [self.termsTitleLabel.topAnchor constraintEqualToAnchor:self.termsOverlay.topAnchor constant:40],
[self.termsTitleLabel.leadingAnchor constraintEqualToAnchor:self.termsOverlay.leadingAnchor constant:20], [self.termsTitleLabel.leadingAnchor constraintEqualToAnchor:self.termsOverlay.leadingAnchor constant:20],
...@@ -243,7 +257,6 @@ ...@@ -243,7 +257,6 @@
]]; ]];
} }
#pragma mark - Labels #pragma mark - Labels
- (void)setupLabels:(UIView *)parent { - (void)setupLabels:(UIView *)parent {
self.unitLabel = [[UILabel alloc] init]; self.unitLabel = [[UILabel alloc] init];
...@@ -450,7 +463,7 @@ ...@@ -450,7 +463,7 @@
#pragma mark - api #pragma mark - api
- (void)requestAppointment { - (void)requestAppointment {
[APIClient requestAppointment:^(NSDictionary *data, NSError *error) { [APIClient requestAppointment:^(BOOL success, NSDictionary *data, NSError *error) {
if (error) return; if (error) return;
if (!data[@"Data"]) return; if (!data[@"Data"]) return;
...@@ -477,14 +490,16 @@ ...@@ -477,14 +490,16 @@
NSArray *slots = selectedTypeDict[@"slots"] ?: @[]; NSArray *slots = selectedTypeDict[@"slots"] ?: @[];
self.allSlots = slots; // store all raw slots self.allSlots = slots; // store all raw slots
// --- 3️⃣ Load Terms HTML --- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (!self.termsAndCondition) {
NSString *termsHTML = selectedTypeDict[@"terms"] ?: @""; NSString *termsHTML = selectedTypeDict[@"terms"] ?: @"";
NSLog(@"termsHTML: %@", termsHTML);
NSString *styledHTML = [NSString stringWithFormat: NSString *styledHTML = [NSString stringWithFormat:
@"<html>" @"<html>"
"<head>" "<head>"
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0\">" "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0\">"
"<style>" "<style>"
"body { font-size: 14px; font-family: -apple-system; line-height: 1.5; padding: 12px; background-color: #F9F9F9; color: #000000; }" "body { font-size: 16px; font-family: -apple-system; line-height: 1.5; padding: 12px; background-color: #F9F9F9; color: #000000; }"
"p, div, span, li { color: #000000 !important; }" "p, div, span, li { color: #000000 !important; }"
"p { margin-bottom: 12px; }" "p { margin-bottom: 12px; }"
"</style>" "</style>"
...@@ -494,17 +509,16 @@ ...@@ -494,17 +509,16 @@
self.termsAndCondition = styledHTML; self.termsAndCondition = styledHTML;
}
dispatch_async(dispatch_get_main_queue(), ^{ [self.termsContainer layoutIfNeeded];
[self.termsWebView loadHTMLString:styledHTML baseURL:nil]; [self.termsWebView layoutIfNeeded];
[self buildMarkedDatesFromSlots:slots]; [self.termsWebView loadHTMLString:self.termsAndCondition baseURL:nil];
[self.calendarView.collectionView reloadData];
}); });
}]; }];
} }
-(void) requestGeneralInfo { -(void) requestGeneralInfo {
[APIClient requestGeneralInfo:^(NSDictionary *data, NSError *error) { [APIClient requestGeneralInfo:^(BOOL success, NSDictionary *data, NSError *error) {
if (error) return; if (error) return;
NSDictionary *generalInfo = data[@"Data"]; NSDictionary *generalInfo = data[@"Data"];
...@@ -705,7 +719,7 @@ ...@@ -705,7 +719,7 @@
dispatch_group_t group = dispatch_group_create(); dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group); dispatch_group_enter(group);
[APIClient requestAppointment:^(NSDictionary *data, NSError *error) { [APIClient requestAppointment:^(BOOL success, NSDictionary *data, NSError *error) {
if (data[@"Data"]) { if (data[@"Data"]) {
NSDictionary *dataDict = data[@"Data"]; NSDictionary *dataDict = data[@"Data"];
self.appointmentData = dataDict; self.appointmentData = dataDict;
...@@ -730,7 +744,7 @@ ...@@ -730,7 +744,7 @@
}]; }];
dispatch_group_enter(group); dispatch_group_enter(group);
[APIClient requestGeneralInfo:^(NSDictionary *data, NSError *error) { [APIClient requestGeneralInfo:^(BOOL success, NSDictionary *data, NSError *error) {
id reps = data[@"Data"][@"representative"]; id reps = data[@"Data"][@"representative"];
self.representativeList = [reps isKindOfClass:[NSArray class]] ? reps : @[]; self.representativeList = [reps isKindOfClass:[NSArray class]] ? reps : @[];
dispatch_group_leave(group); dispatch_group_leave(group);
......
...@@ -927,7 +927,7 @@ ...@@ -927,7 +927,7 @@
- (void)requestDashboardInfo { - (void)requestDashboardInfo {
[self.loadingView startAnimating]; [self.loadingView startAnimating];
[APIClient requestDashboardInfo:^(NSDictionary *data, NSError *error) { [APIClient requestDashboardInfo:^(BOOL success, NSDictionary *data, NSError *error) {
[self.loadingView stopAnimating]; [self.loadingView stopAnimating];
......
...@@ -19,6 +19,7 @@ NS_ASSUME_NONNULL_BEGIN ...@@ -19,6 +19,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, copy) NSString *projectName; @property (nonatomic, copy) NSString *projectName;
@property (nonatomic, copy) NSString *projectId; @property (nonatomic, copy) NSString *projectId;
@property (nonatomic, copy) NSString *drawingPlanId; @property (nonatomic, copy) NSString *drawingPlanId;
@property (nonatomic, copy) NSString *projectLogo;
@end @end
......
...@@ -41,12 +41,13 @@ ...@@ -41,12 +41,13 @@
@property (nonatomic, strong) UILabel *unitNameLabel; @property (nonatomic, strong) UILabel *unitNameLabel;
@property (nonatomic, strong) UIButton *infoButton; @property (nonatomic, strong) UIButton *infoButton;
@property (nonatomic, strong) UIButton *backButton; @property (nonatomic, strong) UIButton *backButton;
@property (nonatomic, strong) UIStackView *contentContainer;
@property (nonatomic, strong) UIView *inspectionBanner;
@end @end
@implementation DashboardViewController { @implementation DashboardViewController {
UIScrollView *_scrollView; UIScrollView *_scrollView;
UIView *_contentContainer, *_inspectionBanner;
NSMutableDictionary *_sectionViews; NSMutableDictionary *_sectionViews;
CGFloat _currentY; CGFloat _currentY;
} }
...@@ -119,13 +120,13 @@ ...@@ -119,13 +120,13 @@
__block NSDictionary *announcementData = nil; __block NSDictionary *announcementData = nil;
dispatch_group_enter(group); dispatch_group_enter(group);
[APIClient requestDashboardInfo:^(NSDictionary *data, NSError *error) { [APIClient requestDashboardInfo:^(BOOL success, NSDictionary *data, NSError *error) {
if (!error) dashboardData = data; if (!error) dashboardData = data;
dispatch_group_leave(group); dispatch_group_leave(group);
}]; }];
dispatch_group_enter(group); dispatch_group_enter(group);
[APIClient requestAnnouncement:^(NSDictionary *data, NSError *error) { [APIClient requestAnnouncement:^(BOOL success, NSDictionary *data, NSError *error) {
if (!error) announcementData = data; if (!error) announcementData = data;
dispatch_group_leave(group); dispatch_group_leave(group);
}]; }];
...@@ -169,10 +170,10 @@ ...@@ -169,10 +170,10 @@
#pragma mark - Header #pragma mark - Header
- (void)setupHeader { - (void)setupHeader {
// Container height
CGFloat headerHeight = 220.0; CGFloat headerHeight = 220.0;
CGFloat maxImageHeight = 220.0;
// Main container // ---------- Header Container ----------
self.headerView = [[UIView alloc] init]; self.headerView = [[UIView alloc] init];
self.headerView.translatesAutoresizingMaskIntoConstraints = NO; self.headerView.translatesAutoresizingMaskIntoConstraints = NO;
self.headerView.backgroundColor = [UIColor colorWithRed:0 green:0.43 blue:0.55 alpha:1.0]; self.headerView.backgroundColor = [UIColor colorWithRed:0 green:0.43 blue:0.55 alpha:1.0];
...@@ -185,40 +186,47 @@ ...@@ -185,40 +186,47 @@
[self.headerView.heightAnchor constraintEqualToConstant:headerHeight] [self.headerView.heightAnchor constraintEqualToConstant:headerHeight]
]]; ]];
// ----------------------- // ---------- Image Container (fixed height) ----------
// Background image UIView *imageContainer = [[UIView alloc] init];
// ----------------------- imageContainer.translatesAutoresizingMaskIntoConstraints = NO;
self.backgroundImageView = [[UIImageView alloc] initWithFrame:CGRectZero]; [self.headerView addSubview:imageContainer];
[NSLayoutConstraint activateConstraints:@[
[imageContainer.topAnchor constraintEqualToAnchor:self.headerView.topAnchor],
[imageContainer.leadingAnchor constraintEqualToAnchor:self.headerView.leadingAnchor],
[imageContainer.trailingAnchor constraintEqualToAnchor:self.headerView.trailingAnchor],
[imageContainer.heightAnchor constraintEqualToConstant:maxImageHeight]
]];
// ---------- Background Image ----------
self.backgroundImageView = [[UIImageView alloc] init];
self.backgroundImageView.translatesAutoresizingMaskIntoConstraints = NO; self.backgroundImageView.translatesAutoresizingMaskIntoConstraints = NO;
self.backgroundImageView.contentMode = UIViewContentModeScaleAspectFill; self.backgroundImageView.contentMode = UIViewContentModeScaleAspectFill;
[self.headerView addSubview:self.backgroundImageView]; self.backgroundImageView.clipsToBounds = YES;
[imageContainer addSubview:self.backgroundImageView];
[NSLayoutConstraint activateConstraints:@[ [NSLayoutConstraint activateConstraints:@[
[self.backgroundImageView.topAnchor constraintEqualToAnchor:self.headerView.topAnchor], [self.backgroundImageView.topAnchor constraintEqualToAnchor:imageContainer.topAnchor],
[self.backgroundImageView.leadingAnchor constraintEqualToAnchor:self.headerView.leadingAnchor], [self.backgroundImageView.leadingAnchor constraintEqualToAnchor:imageContainer.leadingAnchor],
[self.backgroundImageView.trailingAnchor constraintEqualToAnchor:self.headerView.trailingAnchor], [self.backgroundImageView.trailingAnchor constraintEqualToAnchor:imageContainer.trailingAnchor],
[self.backgroundImageView.bottomAnchor constraintEqualToAnchor:self.headerView.bottomAnchor] [self.backgroundImageView.bottomAnchor constraintEqualToAnchor:imageContainer.bottomAnchor]
]]; ]];
// ----------------------- // ---------- Overlay ----------
// Overlay image self.overlayImageView = [[UIImageView alloc] init];
// -----------------------
self.overlayImageView = [[UIImageView alloc] initWithFrame:CGRectZero];
self.overlayImageView.translatesAutoresizingMaskIntoConstraints = NO; self.overlayImageView.translatesAutoresizingMaskIntoConstraints = NO;
self.overlayImageView.contentMode = UIViewContentModeScaleAspectFill; self.overlayImageView.contentMode = UIViewContentModeScaleAspectFill;
self.overlayImageView.alpha = 0.4; self.overlayImageView.alpha = 0.4;
[self.headerView addSubview:self.overlayImageView]; [imageContainer addSubview:self.overlayImageView];
[NSLayoutConstraint activateConstraints:@[ [NSLayoutConstraint activateConstraints:@[
[self.overlayImageView.topAnchor constraintEqualToAnchor:self.headerView.topAnchor], [self.overlayImageView.topAnchor constraintEqualToAnchor:imageContainer.topAnchor],
[self.overlayImageView.leadingAnchor constraintEqualToAnchor:self.headerView.leadingAnchor], [self.overlayImageView.leadingAnchor constraintEqualToAnchor:imageContainer.leadingAnchor],
[self.overlayImageView.trailingAnchor constraintEqualToAnchor:self.headerView.trailingAnchor], [self.overlayImageView.trailingAnchor constraintEqualToAnchor:imageContainer.trailingAnchor],
[self.overlayImageView.bottomAnchor constraintEqualToAnchor:self.headerView.bottomAnchor] [self.overlayImageView.bottomAnchor constraintEqualToAnchor:imageContainer.bottomAnchor]
]]; ]];
// ----------------------- // ---------- Header Bar (Top) ----------
// Header Bar (top area)
// -----------------------
UIView *headerBar = [[UIView alloc] init]; UIView *headerBar = [[UIView alloc] init];
headerBar.translatesAutoresizingMaskIntoConstraints = NO; headerBar.translatesAutoresizingMaskIntoConstraints = NO;
[self.headerView addSubview:headerBar]; [self.headerView addSubview:headerBar];
...@@ -230,13 +238,11 @@ ...@@ -230,13 +238,11 @@
[headerBar.heightAnchor constraintEqualToConstant:44] [headerBar.heightAnchor constraintEqualToConstant:44]
]]; ]];
// ----------------------- // ---------- Project Name ----------
// Project name (center title)
// -----------------------
self.projectNameLabel = [[UILabel alloc] init]; self.projectNameLabel = [[UILabel alloc] init];
self.projectNameLabel.translatesAutoresizingMaskIntoConstraints = NO; self.projectNameLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.projectNameLabel.textColor = UIColor.whiteColor;
self.projectNameLabel.font = [UIFont boldSystemFontOfSize:18]; self.projectNameLabel.font = [UIFont boldSystemFontOfSize:18];
self.projectNameLabel.textColor = UIColor.whiteColor;
self.projectNameLabel.textAlignment = NSTextAlignmentCenter; self.projectNameLabel.textAlignment = NSTextAlignmentCenter;
[headerBar addSubview:self.projectNameLabel]; [headerBar addSubview:self.projectNameLabel];
...@@ -245,7 +251,7 @@ ...@@ -245,7 +251,7 @@
[self.projectNameLabel.centerYAnchor constraintEqualToAnchor:headerBar.centerYAnchor] [self.projectNameLabel.centerYAnchor constraintEqualToAnchor:headerBar.centerYAnchor]
]]; ]];
// --- Menu Button (Top Right) --- // ---------- Menu Button ----------
UIButton *menuButton = [UIButton buttonWithType:UIButtonTypeCustom]; UIButton *menuButton = [UIButton buttonWithType:UIButtonTypeCustom];
menuButton.translatesAutoresizingMaskIntoConstraints = NO; menuButton.translatesAutoresizingMaskIntoConstraints = NO;
[menuButton setImage:[UIImage systemImageNamed:@"line.horizontal.3"] forState:UIControlStateNormal]; [menuButton setImage:[UIImage systemImageNamed:@"line.horizontal.3"] forState:UIControlStateNormal];
...@@ -257,16 +263,29 @@ ...@@ -257,16 +263,29 @@
[menuButton.trailingAnchor constraintEqualToAnchor:headerBar.trailingAnchor constant:-16], [menuButton.trailingAnchor constraintEqualToAnchor:headerBar.trailingAnchor constant:-16],
[menuButton.centerYAnchor constraintEqualToAnchor:headerBar.centerYAnchor], [menuButton.centerYAnchor constraintEqualToAnchor:headerBar.centerYAnchor],
[menuButton.widthAnchor constraintEqualToConstant:26], [menuButton.widthAnchor constraintEqualToConstant:26],
[menuButton.heightAnchor constraintEqualToConstant:26], [menuButton.heightAnchor constraintEqualToConstant:26]
]];
// ---------- Back Button ----------
self.backButton = [UIButton buttonWithType:UIButtonTypeCustom];
self.backButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.backButton setImage:[UIImage systemImageNamed:@"chevron.backward"] forState:UIControlStateNormal];
self.backButton.tintColor = UIColor.whiteColor;
[self.backButton addTarget:self action:@selector(onBack) forControlEvents:UIControlEventTouchUpInside];
[headerBar addSubview:self.backButton];
[NSLayoutConstraint activateConstraints:@[
[self.backButton.leadingAnchor constraintEqualToAnchor:headerBar.leadingAnchor constant:8],
[self.backButton.centerYAnchor constraintEqualToAnchor:headerBar.centerYAnchor],
[self.backButton.widthAnchor constraintEqualToConstant:24],
[self.backButton.heightAnchor constraintEqualToConstant:24]
]]; ]];
// ----------------------- // ---------- Unit Name ----------
// Unit name bottom-left
// -----------------------
self.unitNameLabel = [[UILabel alloc] init]; self.unitNameLabel = [[UILabel alloc] init];
self.unitNameLabel.translatesAutoresizingMaskIntoConstraints = NO; self.unitNameLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.unitNameLabel.textColor = UIColor.whiteColor;
self.unitNameLabel.font = [UIFont boldSystemFontOfSize:20]; self.unitNameLabel.font = [UIFont boldSystemFontOfSize:20];
self.unitNameLabel.textColor = UIColor.whiteColor;
[self.headerView addSubview:self.unitNameLabel]; [self.headerView addSubview:self.unitNameLabel];
[NSLayoutConstraint activateConstraints:@[ [NSLayoutConstraint activateConstraints:@[
...@@ -274,9 +293,7 @@ ...@@ -274,9 +293,7 @@
[self.unitNameLabel.bottomAnchor constraintEqualToAnchor:self.headerView.bottomAnchor constant:-16] [self.unitNameLabel.bottomAnchor constraintEqualToAnchor:self.headerView.bottomAnchor constant:-16]
]]; ]];
// ----------------------- // ---------- Info Button ----------
// Info button bottom-right
// -----------------------
self.infoButton = [UIButton buttonWithType:UIButtonTypeCustom]; self.infoButton = [UIButton buttonWithType:UIButtonTypeCustom];
self.infoButton.translatesAutoresizingMaskIntoConstraints = NO; self.infoButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.infoButton setImage:[UIImage systemImageNamed:@"info.circle"] forState:UIControlStateNormal]; [self.infoButton setImage:[UIImage systemImageNamed:@"info.circle"] forState:UIControlStateNormal];
...@@ -291,11 +308,9 @@ ...@@ -291,11 +308,9 @@
[self.infoButton.heightAnchor constraintEqualToConstant:24] [self.infoButton.heightAnchor constraintEqualToConstant:24]
]]; ]];
// ----------------------- // ---------- Load Remote Image ----------
// Load remote background image NSString *remoteHeaderURL = self.projectLogo;
// ----------------------- if (remoteHeaderURL.length > 0) {
NSString *remoteHeaderURL = @"https://kitadev.commudesk.com/uploads/marketing_photo/164543837127.png";
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:remoteHeaderURL]]; NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:remoteHeaderURL]];
UIImage *img = data ? [UIImage imageWithData:data] : [UIImage imageNamed:@"header_bg"]; UIImage *img = data ? [UIImage imageWithData:data] : [UIImage imageNamed:@"header_bg"];
...@@ -303,37 +318,11 @@ ...@@ -303,37 +318,11 @@
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
self.backgroundImageView.image = img; self.backgroundImageView.image = img;
self.overlayImageView.image = [UIImage imageNamed:@"overlay"]; self.overlayImageView.image = [UIImage imageNamed:@"overlay"];
self.projectNameLabel.text = @"";
self.unitNameLabel.text = @"";
}); });
}); });
} else {
self.backButton = [UIButton buttonWithType:UIButtonTypeCustom]; self.backgroundImageView.image = [UIImage imageNamed:@"header_bg"];
[self.backButton setImage:[UIImage systemImageNamed:@"chevron.backward"] self.overlayImageView.image = [UIImage imageNamed:@"overlay"];
forState:UIControlStateNormal];
self.backButton.tintColor = UIColor.whiteColor;
[self.backButton addTarget:self
action:@selector(onBack)
forControlEvents:UIControlEventTouchUpInside];
self.backButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.headerView addSubview:self.backButton];
[NSLayoutConstraint activateConstraints:@[
[self.backButton.leadingAnchor constraintEqualToAnchor:headerBar.leadingAnchor constant:8],
[self.backButton.centerYAnchor constraintEqualToAnchor:headerBar.centerYAnchor],
[self.backButton.widthAnchor constraintEqualToConstant:24],
[self.backButton.heightAnchor constraintEqualToConstant:24],
]];
}
- (void)handleBackButtonTapped {
NSLog(@"🔙 Back button tapped");
if (self.presentingViewController) {
[self dismissViewControllerAnimated:YES completion:nil];
} else if (self.navigationController) {
[self.navigationController popViewControllerAnimated:YES];
} }
} }
...@@ -424,113 +413,192 @@ ...@@ -424,113 +413,192 @@
#pragma mark - Scroll Content #pragma mark - Scroll Content
- (void)setupScrollContent { - (void)setupScrollContent {
CGFloat topOffset = 220.0; // ScrollView
_scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake( _scrollView = [[UIScrollView alloc] init];
0, _scrollView.translatesAutoresizingMaskIntoConstraints = NO;
topOffset, _scrollView.alwaysBounceVertical = YES;
self.view.bounds.size.width,
self.view.bounds.size.height - topOffset
)];
_scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_scrollView.backgroundColor = [UIColor clearColor];
// 🩹 Add dynamic bottom padding so last card doesn’t clip under footer
CGFloat footerHeight = 80.0 + self.view.safeAreaInsets.bottom;
_scrollView.contentInset = UIEdgeInsetsMake(0, 0, footerHeight, 0);
_scrollView.scrollIndicatorInsets = _scrollView.contentInset;
// === 🌀 Add Pull-to-Refresh ===
UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init];
[refreshControl addTarget:self
action:@selector(handlePullToRefresh:)
forControlEvents:UIControlEventValueChanged];
if (@available(iOS 10.0, *)) {
_scrollView.refreshControl = refreshControl;
} else {
[_scrollView addSubview:refreshControl];
}
[self.view addSubview:_scrollView]; [self.view addSubview:_scrollView];
_contentContainer = [[UIView alloc] initWithFrame:CGRectMake(0, 0, _scrollView.bounds.size.width, 0)]; CGFloat footerHeight = 80.0;
_contentContainer.autoresizingMask = UIViewAutoresizingFlexibleWidth;
[_scrollView addSubview:_contentContainer]; [NSLayoutConstraint activateConstraints:@[
_currentY = 16.0; [_scrollView.topAnchor constraintEqualToAnchor:self.headerView.bottomAnchor],
[self setupInspectionChecklistBanner]; [_scrollView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
if (_currentY == 0) { [_scrollView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
_currentY = 16.0; [_scrollView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:-footerHeight]
]];
UIView *banner = [self buildInspectionChecklistBanner];
UIView *stackContainer = [[UIView alloc] init];
stackContainer.translatesAutoresizingMaskIntoConstraints = NO;
[_scrollView addSubview:stackContainer];
CGFloat topOffset = 16;
if (banner) {
banner.translatesAutoresizingMaskIntoConstraints = NO;
[_scrollView addSubview:banner];
[NSLayoutConstraint activateConstraints:@[
[banner.topAnchor constraintEqualToAnchor:_scrollView.contentLayoutGuide.topAnchor constant:topOffset],
[banner.leadingAnchor constraintEqualToAnchor:_scrollView.leadingAnchor constant:16],
[banner.trailingAnchor constraintEqualToAnchor:_scrollView.trailingAnchor constant:-16],
[banner.heightAnchor constraintEqualToConstant:90]
]];
topOffset += 90 + 16; // stack starts after banner + spacing
} }
// Content container (STACK VIEW)
UIStackView *stack = [[UIStackView alloc] init];
stack.axis = UILayoutConstraintAxisVertical;
stack.spacing = 16;
stack.translatesAutoresizingMaskIntoConstraints = NO;
stack.layoutMargins = UIEdgeInsetsMake(0, 16, 16, 16);
stack.layoutMarginsRelativeArrangement = YES;
[stackContainer addSubview:stack];
self.contentContainer = stack;
[NSLayoutConstraint activateConstraints:@[
[stack.topAnchor constraintEqualToAnchor:stackContainer.topAnchor],
[stack.leadingAnchor constraintEqualToAnchor:stackContainer.leadingAnchor],
[stack.trailingAnchor constraintEqualToAnchor:stackContainer.trailingAnchor],
[stack.bottomAnchor constraintEqualToAnchor:stackContainer.bottomAnchor],
[stack.widthAnchor constraintEqualToAnchor:stackContainer.widthAnchor]
]];
[NSLayoutConstraint activateConstraints:@[
// Top/bottom for vertical scrolling
[stackContainer.topAnchor constraintEqualToAnchor:_scrollView.contentLayoutGuide.topAnchor constant:topOffset],
[stackContainer.bottomAnchor constraintEqualToAnchor:_scrollView.contentLayoutGuide.bottomAnchor],
// ⚡ Width locked to frame to prevent horizontal scroll
[stackContainer.widthAnchor constraintEqualToAnchor:_scrollView.frameLayoutGuide.widthAnchor],
// Leading/trailing optional, can just pin to zero if width is fixed
[stackContainer.leadingAnchor constraintEqualToAnchor:_scrollView.leadingAnchor],
[stackContainer.trailingAnchor constraintEqualToAnchor:_scrollView.trailingAnchor]
]];
} }
#pragma mark - Inspection Checklist Banner #pragma mark - Inspection Checklist Banner
- (void)setupInspectionChecklistBanner { - (UIView *)buildInspectionChecklistBanner {
BOOL blnContactlessHandover = NO; // mock GLOBALS.BLN_CONTACTLESS_HANDOVER == 1 BOOL blnContactlessHandover = NO; // mock GLOBALS.BLN_CONTACTLESS_HANDOVER == 1
BOOL blnInspChecklist = NO; // mock self.bln_InspChecklist BOOL blnInspChecklist = NO; // mock self.bln_InspChecklist
NSString *handoverStatus = @"Pending"; // mock self.strContactlessHandoverStatus NSString *handoverStatus = @"Pending";
NSInteger daysRemaining = 3; // mock self.strDays NSInteger daysRemaining = 3;
// ✅ Condition check // Condition check
if (!(blnContactlessHandover && blnInspChecklist && [handoverStatus isEqualToString:@"Pending"])) { if (!(blnContactlessHandover &&
NSLog(@"ℹ️ No inspection checklist banner required"); blnInspChecklist &&
return; [handoverStatus isEqualToString:@"Pending"])) {
return nil;
} }
// Banner container UIView *banner = [[UIView alloc] init];
_inspectionBanner = [[UIView alloc] initWithFrame:CGRectMake(16, 16, self.view.bounds.size.width - 32, 90)]; banner.translatesAutoresizingMaskIntoConstraints = NO;
_inspectionBanner.backgroundColor = [UIColor colorWithRed:1.0 green:0.39 blue:0.39 alpha:1.0]; // #FF6464 banner.backgroundColor = [UIColor colorWithRed:1.0 green:0.39 blue:0.39 alpha:1.0];
_inspectionBanner.layer.cornerRadius = 20; banner.layer.cornerRadius = 20;
_inspectionBanner.clipsToBounds = YES; banner.clipsToBounds = YES;
// Tap gesture (simulate button press) // Tap gesture
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleInspectionBannerTap)]; UITapGestureRecognizer *tap =
[_inspectionBanner addGestureRecognizer:tap]; [[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(handleInspectionBannerTap)];
// Title label [banner addGestureRecognizer:tap];
UILabel *titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(16, 16, _inspectionBanner.bounds.size.width - 80, 24)]; self.inspectionBanner = banner;
// Title
UILabel *titleLabel = [[UILabel alloc] init];
titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
titleLabel.text = @"Submit Unit Inspection Checklist"; titleLabel.text = @"Submit Unit Inspection Checklist";
titleLabel.font = [UIFont boldSystemFontOfSize:16]; titleLabel.font = [UIFont boldSystemFontOfSize:16];
titleLabel.textColor = UIColor.whiteColor; titleLabel.textColor = UIColor.whiteColor;
[_inspectionBanner addSubview:titleLabel]; titleLabel.numberOfLines = 2;
[banner addSubview:titleLabel];
// Right arrow icon // Arrow
UIImageView *arrow = [[UIImageView alloc] initWithImage:[UIImage systemImageNamed:@"chevron.right"]]; UIImageView *arrow = [[UIImageView alloc]
initWithImage:[UIImage systemImageNamed:@"chevron.right"]];
arrow.translatesAutoresizingMaskIntoConstraints = NO;
arrow.tintColor = UIColor.whiteColor; arrow.tintColor = UIColor.whiteColor;
arrow.frame = CGRectMake(_inspectionBanner.bounds.size.width - 36, (_inspectionBanner.bounds.size.height - 24)/2, 20, 24); [banner addSubview:arrow];
arrow.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
[_inspectionBanner addSubview:arrow]; UIView *pill = nil;
// Days remaining pill (optional)
if (daysRemaining > 0) { if (daysRemaining > 0) {
UIView *pill = [[UIView alloc] initWithFrame:CGRectMake(16, CGRectGetMaxY(titleLabel.frame) + 6, 170, 28)]; pill = [[UIView alloc] init];
pill.translatesAutoresizingMaskIntoConstraints = NO;
pill.backgroundColor = [UIColor colorWithWhite:0 alpha:0.4]; pill.backgroundColor = [UIColor colorWithWhite:0 alpha:0.4];
pill.layer.cornerRadius = 14; pill.layer.cornerRadius = 14;
pill.clipsToBounds = YES; pill.clipsToBounds = YES;
[banner addSubview:pill];
UIImageView *calendarIcon = [[UIImageView alloc] initWithImage:[UIImage systemImageNamed:@"calendar"]]; UIImageView *calendarIcon = [[UIImageView alloc]
initWithImage:[UIImage systemImageNamed:@"calendar"]];
calendarIcon.translatesAutoresizingMaskIntoConstraints = NO;
calendarIcon.tintColor = UIColor.whiteColor; calendarIcon.tintColor = UIColor.whiteColor;
calendarIcon.frame = CGRectMake(8, 4, 20, 20);
[pill addSubview:calendarIcon]; [pill addSubview:calendarIcon];
UILabel *daysLabel = [[UILabel alloc] initWithFrame:CGRectMake(34, 4, 130, 20)]; UILabel *daysLabel = [[UILabel alloc] init];
daysLabel.text = [NSString stringWithFormat:@"%ld days remaining", (long)daysRemaining]; daysLabel.translatesAutoresizingMaskIntoConstraints = NO;
daysLabel.text =
[NSString stringWithFormat:@"%ld days remaining", (long)daysRemaining];
daysLabel.textColor = UIColor.whiteColor; daysLabel.textColor = UIColor.whiteColor;
daysLabel.font = [UIFont boldSystemFontOfSize:13]; daysLabel.font = [UIFont boldSystemFontOfSize:13];
[pill addSubview:daysLabel]; [pill addSubview:daysLabel];
[_inspectionBanner addSubview:pill]; [NSLayoutConstraint activateConstraints:@[
[calendarIcon.leadingAnchor constraintEqualToAnchor:pill.leadingAnchor constant:8],
[calendarIcon.centerYAnchor constraintEqualToAnchor:pill.centerYAnchor],
[calendarIcon.widthAnchor constraintEqualToConstant:20],
[calendarIcon.heightAnchor constraintEqualToConstant:20],
[daysLabel.leadingAnchor constraintEqualToAnchor:calendarIcon.trailingAnchor constant:6],
[daysLabel.trailingAnchor constraintEqualToAnchor:pill.trailingAnchor constant:-10],
[daysLabel.centerYAnchor constraintEqualToAnchor:pill.centerYAnchor],
]];
} }
[_contentContainer addSubview:_inspectionBanner]; // Banner layout
_currentY = CGRectGetMaxY(_inspectionBanner.frame) + 16.0; NSMutableArray *constraints = [@[
[banner.heightAnchor constraintGreaterThanOrEqualToConstant:90],
[titleLabel.topAnchor constraintEqualToAnchor:banner.topAnchor constant:16],
[titleLabel.leadingAnchor constraintEqualToAnchor:banner.leadingAnchor constant:16],
[titleLabel.trailingAnchor constraintEqualToAnchor:arrow.leadingAnchor constant:-8],
[arrow.centerYAnchor constraintEqualToAnchor:banner.centerYAnchor],
[arrow.trailingAnchor constraintEqualToAnchor:banner.trailingAnchor constant:-16],
[arrow.widthAnchor constraintEqualToConstant:20],
[arrow.heightAnchor constraintEqualToConstant:24],
] mutableCopy];
if (pill) {
[constraints addObjectsFromArray:@[
[pill.topAnchor constraintEqualToAnchor:titleLabel.bottomAnchor constant:8],
[pill.leadingAnchor constraintEqualToAnchor:banner.leadingAnchor constant:16],
[pill.heightAnchor constraintEqualToConstant:28],
[pill.bottomAnchor constraintEqualToAnchor:banner.bottomAnchor constant:-16]
]];
} else {
[constraints addObject:
[titleLabel.bottomAnchor constraintEqualToAnchor:banner.bottomAnchor constant:-16]];
}
[NSLayoutConstraint activateConstraints:constraints];
return banner;
} }
- (void)handleInspectionBannerTap { - (void)handleInspectionBannerTap {
NSLog(@"🟥 Inspection checklist banner tapped"); NSLog(@"🟥 Inspection checklist banner tapped");
[UIView animateWithDuration:0.1 animations:^{ [UIView animateWithDuration:0.1 animations:^{
_inspectionBanner.alpha = 0.6; self.inspectionBanner.alpha = 0.6;
} completion:^(BOOL finished) { } completion:^(BOOL finished) {
[UIView animateWithDuration:0.1 animations:^{ [UIView animateWithDuration:0.1 animations:^{
_inspectionBanner.alpha = 1.0; self.inspectionBanner.alpha = 1.0;
}]; }];
}]; }];
} }
...@@ -649,219 +717,280 @@ ...@@ -649,219 +717,280 @@
NSLog(@"🔄 Refreshing dashboard sections..."); NSLog(@"🔄 Refreshing dashboard sections...");
// Remove any old views from scroll // Remove any old views from scroll
for (UIView *view in _contentContainer.subviews) { for (UIView *view in self.contentContainer.arrangedSubviews) {
[self.contentContainer removeArrangedSubview:view];
[view removeFromSuperview]; [view removeFromSuperview];
} }
_currentY = 16.0;
// === PROJECT UPDATES === // === PROJECT UPDATES ===
// NSArray *announcements = self.announcements; NSArray *announcements = self.announcements;
// if (announcements.count > 0) { if (announcements.count > 0) {
// UIView *announcementSection = [self buildAnnouncementSection:announcements]; UIView *announcementSection = [self buildAnnouncementSection:announcements];
// CGRect f = announcementSection.frame; if (announcementSection) {
// f.origin.y = _currentY; [self.contentContainer addArrangedSubview:announcementSection];
// announcementSection.frame = f; }
// [_contentContainer addSubview:announcementSection]; }
// _currentY += CGRectGetHeight(announcementSection.frame) + 12;
// }
// === APPOINTMENTS === // === APPOINTMENTS ===
NSArray *appointments = mainData[@"appointment"]; NSArray *appointments = mainData[@"appointment"];
if (appointments.count > 0) { if (appointments.count > 0) {
UIView *appointmentSection = [self buildAppointmentSection:appointments]; UIView *appointmentSection = [self buildAppointmentSection:appointments];
CGRect f = appointmentSection.frame; if (appointmentSection) {
f.origin.y = _currentY; [self.contentContainer addArrangedSubview:appointmentSection];
appointmentSection.frame = f; }
[_contentContainer addSubview:appointmentSection];
_currentY += CGRectGetHeight(appointmentSection.frame) + 12;
} }
// === ISSUE UPDATES === // === ISSUE UPDATES ===
NSArray *issues = mainData[@"issue_update"]; NSArray *issues = mainData[@"issue_update"];
if (issues.count > 0) { if (issues.count > 0) {
UIView *issueSection = [self buildIssueSection:issues]; UIView *issueSection = [self buildIssueSection:issues];
CGRect f = issueSection.frame; if (issueSection) {
f.origin.y = _currentY; [self.contentContainer addArrangedSubview:issueSection];
issueSection.frame = f; }
[_contentContainer addSubview:issueSection];
_currentY += CGRectGetHeight(issueSection.frame) + 16;
} }
_contentContainer.frame = CGRectMake(0, 0, _scrollView.bounds.size.width, _currentY); // Bottom padding so content doesn't kiss footer
_scrollView.contentSize = CGSizeMake(_scrollView.bounds.size.width, _currentY); UIView *bottomSpacer = [[UIView alloc] init];
[bottomSpacer.heightAnchor constraintEqualToConstant:24].active = YES;
NSLog(@"✅ Dashboard UI refreshed with %lu announcements, %lu appointments, %lu issues", [self.contentContainer addArrangedSubview:bottomSpacer];
// (unsigned long)announcements.count,
(unsigned long)appointments.count, (unsigned long)issues.count);
} }
#pragma mark - Announcement Section
- (UIView *)buildAnnouncementSection:(NSArray *)announcements { - (UIView *)buildAnnouncementSection:(NSArray *)announcements {
UIView *section = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, 160)]; UIView *section = [[UIView alloc] init];
section.translatesAutoresizingMaskIntoConstraints = NO;
// --- Title row --- [self.contentContainer addSubview:section]; // MUST come first
UILabel *title = [[UILabel alloc] initWithFrame:CGRectMake(16, 0, 200, 24)];
title.text = @"Project Update";
title.font = [UIFont boldSystemFontOfSize:18];
title.textColor = [UIColor blackColor];
title.userInteractionEnabled = YES; // 👈 enable taps
UITapGestureRecognizer *titleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(sectionHeaderTapped:)]; [NSLayoutConstraint activateConstraints:@[
[title addGestureRecognizer:titleTap]; [section.leadingAnchor constraintEqualToAnchor:self.contentContainer.leadingAnchor],
[section addSubview:title]; [section.trailingAnchor constraintEqualToAnchor:self.contentContainer.trailingAnchor]
]];
UIImageView *arrow = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"icon_green_arrowright"]];
arrow.frame = CGRectMake(self.view.bounds.size.width - 40, 2, 24, 24);
[section addSubview:arrow];
// --- Horizontal scroll for cards ---
UIScrollView *hScroll = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 40, self.view.bounds.size.width, 110)];
hScroll.showsHorizontalScrollIndicator = NO;
CGFloat x = 16;
NSArray *limitedAnnouncements = (announcements.count > 5)
? [announcements subarrayWithRange:NSMakeRange(0, 5)]
: announcements;
for (NSDictionary *item in limitedAnnouncements) { // --- Header ---
UIView *card = [self makeAnnouncementCard:item]; UIButton *headerButton = [UIButton buttonWithType:UIButtonTypeCustom];
CGRect frame = card.frame; headerButton.translatesAutoresizingMaskIntoConstraints = NO;
frame.origin.x = x; [section addSubview:headerButton];
frame.origin.y = 0;
card.frame = frame;
[hScroll addSubview:card];
x += frame.size.width + 12;
}
hScroll.contentSize = CGSizeMake(x, 110);
[section addSubview:hScroll];
return section; // [headerButton addTarget:self
} // action:@selector(appointmentHeaderTapped:)
// forControlEvents:UIControlEventTouchUpInside];
UILabel *title = [[UILabel alloc] init];
title.translatesAutoresizingMaskIntoConstraints = NO;
title.text = @"Project Updates";
title.font = [UIFont boldSystemFontOfSize:18];
title.textColor = UIColor.blackColor;
title.userInteractionEnabled = NO; // important
[headerButton addSubview:title];
- (UIView *)buildAppointmentSection:(NSArray *)appointments { UIImageView *arrow = [[UIImageView alloc] initWithImage:
UIView *section = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, 0)]; [UIImage systemImageNamed:@"chevron.forward"]];
arrow.translatesAutoresizingMaskIntoConstraints = NO;
arrow.userInteractionEnabled = NO;
arrow.tintColor = [UIColor colorWithRed:0.0 green:0.5 blue:0.6 alpha:1.0];;
[headerButton addSubview:arrow];
UILabel *title = [[UILabel alloc] initWithFrame:CGRectMake(16, 0, 200, 24)]; [NSLayoutConstraint activateConstraints:@[
title.text = @"Appointments"; [headerButton.topAnchor constraintEqualToAnchor:section.topAnchor],
title.font = [UIFont boldSystemFontOfSize:18]; [headerButton.leadingAnchor constraintEqualToAnchor:section.leadingAnchor],
[section addSubview:title]; [headerButton.trailingAnchor constraintEqualToAnchor:section.trailingAnchor],
title.textColor = [UIColor blackColor]; [headerButton.heightAnchor constraintEqualToConstant:44],
title.userInteractionEnabled = YES; // 👈 enable taps
[title.centerYAnchor constraintEqualToAnchor:headerButton.centerYAnchor],
UITapGestureRecognizer *titleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(appointmentHeaderTapped:)]; [title.leadingAnchor constraintEqualToAnchor:headerButton.leadingAnchor constant:16],
[title addGestureRecognizer:titleTap];
UIImageView *arrow = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"icon_green_arrowright"]]; [arrow.centerYAnchor constraintEqualToAnchor:title.centerYAnchor],
arrow.frame = CGRectMake(self.view.bounds.size.width - 40, 2, 24, 24); [arrow.trailingAnchor constraintEqualToAnchor:title.trailingAnchor constant:20],
[section addSubview:arrow]; [arrow.widthAnchor constraintEqualToConstant:12],
[arrow.heightAnchor constraintEqualToConstant:18],
CGFloat y = 34; ]];
CGFloat cardWidth = self.view.bounds.size.width * 0.9; // 90% of screen width
CGFloat xCenter = (self.view.bounds.size.width - cardWidth) / 2.0;
NSArray *limitedAppointments = (appointments.count > 3)
? [appointments subarrayWithRange:NSMakeRange(0, 3)]
: appointments;
for (NSDictionary *item in limitedAppointments) {
UIView *card = [self makeAppointmentCard:item width:cardWidth];
CGRect frame = card.frame;
frame.origin.x = xCenter;
frame.origin.y = y;
card.frame = frame;
[section addSubview:card];
y += frame.size.height + 12; // spacing between cards
}
section.frame = CGRectMake(0, 0, self.view.bounds.size.width, y + 10); // --- Horizontal Scroll ---
return section; UIScrollView *hScroll = [[UIScrollView alloc] init];
} hScroll.translatesAutoresizingMaskIntoConstraints = NO;
hScroll.showsHorizontalScrollIndicator = NO;
[section addSubview:hScroll];
- (UIView *)buildIssueSection:(NSArray *)issues { [NSLayoutConstraint activateConstraints:@[
UIView *section = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, 0)]; [hScroll.topAnchor constraintEqualToAnchor:title.bottomAnchor constant:12],
[hScroll.leadingAnchor constraintEqualToAnchor:section.leadingAnchor],
[hScroll.trailingAnchor constraintEqualToAnchor:section.trailingAnchor],
[hScroll.heightAnchor constraintEqualToConstant:90],
[hScroll.bottomAnchor constraintEqualToAnchor:section.bottomAnchor] // important
]];
UILabel *title = [[UILabel alloc] initWithFrame:CGRectMake(16, 0, 200, 24)]; UIStackView *cards = [[UIStackView alloc] init];
title.text = @"Issue Updates"; cards.axis = UILayoutConstraintAxisHorizontal;
title.font = [UIFont boldSystemFontOfSize:18]; cards.spacing = 12;
[section addSubview:title]; cards.translatesAutoresizingMaskIntoConstraints = NO;
title.textColor = [UIColor blackColor]; [hScroll addSubview:cards];
title.userInteractionEnabled = YES; // 👈 enable taps
UITapGestureRecognizer *titleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(issueHeaderTapped:)]; [NSLayoutConstraint activateConstraints:@[
[title addGestureRecognizer:titleTap]; [cards.topAnchor constraintEqualToAnchor:hScroll.topAnchor],
UIImageView *arrow = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"icon_green_arrowright"]]; [cards.bottomAnchor constraintEqualToAnchor:hScroll.bottomAnchor],
arrow.frame = CGRectMake(self.view.bounds.size.width - 40, 2, 24, 24); [cards.leadingAnchor constraintEqualToAnchor:hScroll.leadingAnchor constant:16],
[section addSubview:arrow]; [cards.trailingAnchor constraintEqualToAnchor:hScroll.trailingAnchor constant:-16],
[cards.heightAnchor constraintEqualToAnchor:hScroll.heightAnchor]
]];
CGFloat y = 34; NSArray *limited = announcements.count > 5 ? [announcements subarrayWithRange:NSMakeRange(0, 5)] : announcements;
CGFloat cardWidth = self.view.bounds.size.width * 0.9; // 90% of screen width for (NSDictionary *item in limited) {
CGFloat xCenter = (self.view.bounds.size.width - cardWidth) / 2.0; UIView *card = [self makeAnnouncementCard:item];
card.translatesAutoresizingMaskIntoConstraints = NO;
[cards addArrangedSubview:card];
for (NSDictionary *item in issues) {
UIView *card = [self makeIssueCard:item width:cardWidth];
CGRect frame = card.frame;
frame.origin.x = xCenter;
frame.origin.y = y;
card.frame = frame;
[section addSubview:card];
y += frame.size.height + 12;
} }
section.frame = CGRectMake(0, 0, self.view.bounds.size.width, y + 10);
return section; return section;
} }
- (UIView *)makeAnnouncementCard:(NSDictionary *)item { - (UIView *)makeAnnouncementCard:(NSDictionary *)item {
UIView *card = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 280, 100)]; UIView *card = [[UIView alloc] init];
card.translatesAutoresizingMaskIntoConstraints = NO;
card.backgroundColor = UIColor.whiteColor; card.backgroundColor = UIColor.whiteColor;
card.layer.cornerRadius = 8; card.layer.cornerRadius = 8;
card.layer.shadowColor = [UIColor colorWithWhite:0 alpha:0.1].CGColor; card.layer.shadowColor = [UIColor colorWithWhite:0 alpha:0.1].CGColor;
card.layer.shadowOpacity = 0.3; card.layer.shadowOpacity = 0.3;
card.layer.shadowOffset = CGSizeMake(0, 1); card.layer.shadowOffset = CGSizeMake(0, 1);
// === 1. Resolve the date field (support both created_at and date) === // Fixed card size (horizontal scroll needs this)
[NSLayoutConstraint activateConstraints:@[
[card.widthAnchor constraintEqualToConstant:272],
[card.heightAnchor constraintEqualToConstant:80]
]];
// Resolve date
NSString *dateString = item[@"created_at"] ?: item[@"date"]; NSString *dateString = item[@"created_at"] ?: item[@"date"];
NSString *dateOnly = dateString; NSString *dateOnly = dateString;
if ([dateOnly containsString:@" "]) {
// === take only the date portion (before space) === dateOnly = [[dateOnly componentsSeparatedByString:@" "] firstObject];
if ([dateString containsString:@" "]) {
dateOnly = [[dateString componentsSeparatedByString:@" "] firstObject];
} }
// === format it ===
NSDateFormatter *fmt = [[NSDateFormatter alloc] init]; NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
fmt.dateFormat = @"yyyy-MM-dd"; fmt.dateFormat = @"yyyy-MM-dd";
NSDate *dateObj = [fmt dateFromString:dateOnly]; NSDate *dateObj = [fmt dateFromString:dateOnly];
fmt.dateFormat = @"dd/MM/yyyy"; fmt.dateFormat = @"dd/MM/yyyy";
UILabel *date = [[UILabel alloc] initWithFrame:CGRectMake(12, 8, 200, 16)]; UILabel *date = [[UILabel alloc] init];
date.translatesAutoresizingMaskIntoConstraints = NO;
date.text = dateObj ? [fmt stringFromDate:dateObj] : @"--/--/----"; date.text = dateObj ? [fmt stringFromDate:dateObj] : @"--/--/----";
date.textColor = [UIColor grayColor];
date.font = [UIFont systemFontOfSize:13]; date.font = [UIFont systemFontOfSize:13];
date.textColor = UIColor.grayColor;
[card addSubview:date]; [card addSubview:date];
// === 2. Title === UILabel *title = [[UILabel alloc] init];
UILabel *title = [[UILabel alloc] initWithFrame:CGRectMake(12, 28, 256, 60)]; title.translatesAutoresizingMaskIntoConstraints = NO;
title.text = item[@"title"] ?: @"Untitled announcement"; title.text = item[@"title"] ?: @"Untitled announcement";
title.numberOfLines = 3;
title.font = [UIFont boldSystemFontOfSize:16]; title.font = [UIFont boldSystemFontOfSize:16];
title.textColor = [UIColor blackColor]; title.numberOfLines = 3;
title.textColor = UIColor.blackColor;
[card addSubview:title]; [card addSubview:title];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleAnnouncementTap:)]; [NSLayoutConstraint activateConstraints:@[
[date.topAnchor constraintEqualToAnchor:card.topAnchor constant:8],
[date.leadingAnchor constraintEqualToAnchor:card.leadingAnchor constant:12],
[title.topAnchor constraintEqualToAnchor:date.bottomAnchor constant:4],
[title.leadingAnchor constraintEqualToAnchor:card.leadingAnchor constant:12],
[title.trailingAnchor constraintEqualToAnchor:card.trailingAnchor constant:-12],
[title.bottomAnchor constraintLessThanOrEqualToAnchor:card.bottomAnchor constant:-8]
]];
UITapGestureRecognizer *tap =
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleAnnouncementTap:)];
[card addGestureRecognizer:tap]; [card addGestureRecognizer:tap];
return card; return card;
} }
- (UIView *)makeAppointmentCard:(NSDictionary *)item width:(CGFloat)width { #pragma mark - Appointment Section
UIView *card = [[UIView alloc] initWithFrame:CGRectMake(0, 0, width, 90)]; - (UIView *)buildAppointmentSection:(NSArray *)appointments {
UIView *section = [[UIView alloc] init];
section.translatesAutoresizingMaskIntoConstraints = NO;
[self.contentContainer addSubview:section];
[NSLayoutConstraint activateConstraints:@[
[section.leadingAnchor constraintEqualToAnchor:self.contentContainer.leadingAnchor],
[section.trailingAnchor constraintEqualToAnchor:self.contentContainer.trailingAnchor]
]];
// Header
UIButton *headerButton = [UIButton buttonWithType:UIButtonTypeCustom];
headerButton.translatesAutoresizingMaskIntoConstraints = NO;
[section addSubview:headerButton];
[headerButton addTarget:self
action:@selector(appointmentHeaderTapped:)
forControlEvents:UIControlEventTouchUpInside];
UILabel *title = [[UILabel alloc] init];
title.translatesAutoresizingMaskIntoConstraints = NO;
title.text = @"Appointments";
title.font = [UIFont boldSystemFontOfSize:18];
title.textColor = UIColor.blackColor;
title.userInteractionEnabled = NO; // important
[headerButton addSubview:title];
UIImageView *arrow = [[UIImageView alloc] initWithImage:
[UIImage systemImageNamed:@"chevron.forward"]];
arrow.translatesAutoresizingMaskIntoConstraints = NO;
arrow.userInteractionEnabled = NO;
arrow.tintColor = [UIColor colorWithRed:0.0 green:0.5 blue:0.6 alpha:1.0];
[headerButton addSubview:arrow];
[NSLayoutConstraint activateConstraints:@[
[headerButton.topAnchor constraintEqualToAnchor:section.topAnchor],
[headerButton.leadingAnchor constraintEqualToAnchor:section.leadingAnchor],
[headerButton.trailingAnchor constraintEqualToAnchor:section.trailingAnchor],
[headerButton.heightAnchor constraintEqualToConstant:44],
[title.centerYAnchor constraintEqualToAnchor:headerButton.centerYAnchor],
[title.leadingAnchor constraintEqualToAnchor:headerButton.leadingAnchor constant:16],
[arrow.centerYAnchor constraintEqualToAnchor:title.centerYAnchor],
[arrow.trailingAnchor constraintEqualToAnchor:title.trailingAnchor constant:25],
[arrow.widthAnchor constraintEqualToConstant:12],
[arrow.heightAnchor constraintEqualToConstant:18],
]];
// Vertical stack for cards
UIStackView *list = [[UIStackView alloc] init];
list.axis = UILayoutConstraintAxisVertical;
list.spacing = 12;
list.translatesAutoresizingMaskIntoConstraints = NO;
list.layoutMargins = UIEdgeInsetsMake(10, 16, 16, 16); // consistent padding
list.layoutMarginsRelativeArrangement = YES;
list.alignment = UIStackViewAlignmentFill; // fill width
[section addSubview:list];
[NSLayoutConstraint activateConstraints:@[
[list.topAnchor constraintEqualToAnchor:title.bottomAnchor],
[list.leadingAnchor constraintEqualToAnchor:section.leadingAnchor],
[list.trailingAnchor constraintEqualToAnchor:section.trailingAnchor],
[list.bottomAnchor constraintEqualToAnchor:section.bottomAnchor]
]];
NSArray *limited = appointments.count > 3 ? [appointments subarrayWithRange:NSMakeRange(0, 3)] : appointments;
for (NSDictionary *item in limited) {
UIView *card = [self makeAppointmentCard:item];
[list addArrangedSubview:card];
}
return section;
}
- (UIView *)makeAppointmentCard:(NSDictionary *)item {
UIView *card = [[UIView alloc] init];
card.translatesAutoresizingMaskIntoConstraints = NO;
card.backgroundColor = UIColor.whiteColor; card.backgroundColor = UIColor.whiteColor;
card.layer.cornerRadius = 8; card.layer.cornerRadius = 8;
card.layer.shadowColor = [UIColor colorWithWhite:0 alpha:0.1].CGColor; card.layer.shadowColor = [UIColor colorWithWhite:0 alpha:0.1].CGColor;
card.layer.shadowOpacity = 0.3; card.layer.shadowOpacity = 0.3;
card.layer.shadowOffset = CGSizeMake(0, 1); card.layer.shadowOffset = CGSizeMake(0, 1);
// Extract fields [card.heightAnchor constraintEqualToConstant:90].active = YES;
NSString *appointmentType = item[@"appointment_type"] ?: @"-"; NSString *appointmentType = item[@"appointment_type"] ?: @"-";
NSString *status = item[@"status"] ?: @"-"; NSString *status = item[@"status"] ?: @"-";
NSString *startDate = item[@"start_date"] ?: @"-"; NSString *startDate = item[@"start_date"] ?: @"-";
...@@ -869,152 +998,259 @@ ...@@ -869,152 +998,259 @@
NSString *buyerName = @"-"; NSString *buyerName = @"-";
NSArray *buyers = item[@"primary_buyer"]; NSArray *buyers = item[@"primary_buyer"];
if ([buyers isKindOfClass:[NSArray class]] && buyers.count > 0) { if ([buyers isKindOfClass:NSArray.class] && buyers.count) {
NSDictionary *buyer = buyers.firstObject; buyerName = buyers.firstObject[@"name"] ?: @"-";
if ([buyer[@"name"] isKindOfClass:[NSString class]]) {
buyerName = buyer[@"name"];
}
} }
// Notification Icon UIView *iconCircle = [[UIView alloc] init];
UIView *iconCircle = [[UIView alloc] initWithFrame:CGRectMake(12, 14, 24, 24)]; iconCircle.translatesAutoresizingMaskIntoConstraints = NO;
iconCircle.backgroundColor = [UIColor colorWithRed:1.0 green:0.4 blue:0.4 alpha:1.0]; iconCircle.backgroundColor = [UIColor colorWithRed:1.0 green:0.4 blue:0.4 alpha:1.0];
iconCircle.layer.cornerRadius = 12; iconCircle.layer.cornerRadius = 12;
[card addSubview:iconCircle];
UIImageView *bell = [[UIImageView alloc] initWithImage:[UIImage systemImageNamed:@"bell.fill"]]; UIImageView *bell = [[UIImageView alloc] initWithImage:[UIImage systemImageNamed:@"bell.fill"]];
bell.translatesAutoresizingMaskIntoConstraints = NO;
bell.tintColor = UIColor.whiteColor; bell.tintColor = UIColor.whiteColor;
bell.frame = CGRectMake(4, 4, 16, 16);
[iconCircle addSubview:bell]; [iconCircle addSubview:bell];
[card addSubview:iconCircle];
// Status UILabel *statusLabel = [[UILabel alloc] init];
UILabel *statusLabel = [[UILabel alloc] initWithFrame:CGRectMake(44, 12, 180, 26)]; statusLabel.translatesAutoresizingMaskIntoConstraints = NO;
statusLabel.text = status; statusLabel.text = status;
statusLabel.font = [UIFont boldSystemFontOfSize:17]; statusLabel.font = [UIFont boldSystemFontOfSize:17];
statusLabel.textColor = [UIColor colorWithRed:0.0 green:0.43 blue:0.55 alpha:1.0]; statusLabel.textColor = [UIColor colorWithRed:0 green:0.43 blue:0.55 alpha:1];
[card addSubview:statusLabel]; [card addSubview:statusLabel];
// Time UILabel *timeLabel = [[UILabel alloc] init];
UILabel *timeLabel = [[UILabel alloc] initWithFrame:CGRectMake(width - 90, 12, 80, 26)]; timeLabel.translatesAutoresizingMaskIntoConstraints = NO;
timeLabel.textAlignment = NSTextAlignmentRight;
timeLabel.text = startTime; timeLabel.text = startTime;
timeLabel.textAlignment = NSTextAlignmentRight;
timeLabel.font = [UIFont boldSystemFontOfSize:15]; timeLabel.font = [UIFont boldSystemFontOfSize:15];
timeLabel.textColor = [UIColor grayColor]; timeLabel.textColor = UIColor.grayColor;
[card addSubview:timeLabel]; [card addSubview:timeLabel];
// Description UILabel *desc = [[UILabel alloc] init];
NSString *desc = [NSString stringWithFormat:@"You have a %@ appointment on %@, %@", appointmentType, startDate, buyerName]; desc.translatesAutoresizingMaskIntoConstraints = NO;
desc.text = [NSString stringWithFormat:
@"You have a %@ appointment on %@, %@", appointmentType, startDate, buyerName];
desc.font = [UIFont systemFontOfSize:15];
desc.numberOfLines = 2;
[card addSubview:desc];
UILabel *descLabel = [[UILabel alloc] initWithFrame:CGRectMake(16, 46, width - 32, 36)]; [NSLayoutConstraint activateConstraints:@[
descLabel.text = desc; [iconCircle.leadingAnchor constraintEqualToAnchor:card.leadingAnchor constant:12],
descLabel.font = [UIFont systemFontOfSize:15]; [iconCircle.topAnchor constraintEqualToAnchor:card.topAnchor constant:14],
descLabel.numberOfLines = 2; [iconCircle.widthAnchor constraintEqualToConstant:24],
descLabel.textColor = [UIColor blackColor]; [iconCircle.heightAnchor constraintEqualToConstant:24],
[card addSubview:descLabel];
[bell.centerXAnchor constraintEqualToAnchor:iconCircle.centerXAnchor],
[bell.centerYAnchor constraintEqualToAnchor:iconCircle.centerYAnchor],
[bell.widthAnchor constraintEqualToConstant:16],
[bell.heightAnchor constraintEqualToConstant:16],
[statusLabel.leadingAnchor constraintEqualToAnchor:iconCircle.trailingAnchor constant:8],
[statusLabel.topAnchor constraintEqualToAnchor:card.topAnchor constant:12],
[timeLabel.trailingAnchor constraintEqualToAnchor:card.trailingAnchor constant:-12],
[timeLabel.centerYAnchor constraintEqualToAnchor:statusLabel.centerYAnchor],
[desc.topAnchor constraintEqualToAnchor:statusLabel.bottomAnchor constant:6],
[desc.leadingAnchor constraintEqualToAnchor:card.leadingAnchor constant:16],
[desc.trailingAnchor constraintEqualToAnchor:card.trailingAnchor constant:-16],
[desc.bottomAnchor constraintLessThanOrEqualToAnchor:card.bottomAnchor constant:-8]
]];
// Store appointment data so tap handler can retrieve it
objc_setAssociatedObject(card, "appointmentData", item, OBJC_ASSOCIATION_RETAIN_NONATOMIC); objc_setAssociatedObject(card, "appointmentData", item, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// Tap UITapGestureRecognizer *tap =
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleAppointmentTap:)]; [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleAppointmentTap:)];
[card addGestureRecognizer:tap]; [card addGestureRecognizer:tap];
return card; return card;
} }
- (UIView *)makeIssueCard:(NSDictionary *)item width:(CGFloat)width { #pragma mark - Issue Section
UIView *card = [[UIView alloc] initWithFrame:CGRectMake(0, 0, width, 110)]; - (UIView *)buildIssueSection:(NSArray *)issues {
UIView *section = [[UIView alloc] init];
section.translatesAutoresizingMaskIntoConstraints = NO;
[self.contentContainer addSubview:section];
[NSLayoutConstraint activateConstraints:@[
[section.leadingAnchor constraintEqualToAnchor:self.contentContainer.leadingAnchor],
[section.trailingAnchor constraintEqualToAnchor:self.contentContainer.trailingAnchor]
]];
// Header
UIButton *headerButton = [UIButton buttonWithType:UIButtonTypeCustom];
headerButton.translatesAutoresizingMaskIntoConstraints = NO;
[section addSubview:headerButton];
[headerButton addTarget:self
action:@selector(issueHeaderTapped:)
forControlEvents:UIControlEventTouchUpInside];
UILabel *title = [[UILabel alloc] init];
title.translatesAutoresizingMaskIntoConstraints = NO;
title.text = @"Issue Updates";
title.font = [UIFont boldSystemFontOfSize:18];
title.textColor = UIColor.blackColor;
title.userInteractionEnabled = NO; // important
[headerButton addSubview:title];
UIImageView *arrow = [[UIImageView alloc] initWithImage:
[UIImage systemImageNamed:@"chevron.forward"]];
arrow.translatesAutoresizingMaskIntoConstraints = NO;
arrow.userInteractionEnabled = NO;
arrow.tintColor = [UIColor colorWithRed:0.0 green:0.5 blue:0.6 alpha:1.0];
[headerButton addSubview:arrow];
[NSLayoutConstraint activateConstraints:@[
[headerButton.topAnchor constraintEqualToAnchor:section.topAnchor],
[headerButton.leadingAnchor constraintEqualToAnchor:section.leadingAnchor],
[headerButton.trailingAnchor constraintEqualToAnchor:section.trailingAnchor],
[headerButton.heightAnchor constraintEqualToConstant:44],
[title.centerYAnchor constraintEqualToAnchor:headerButton.centerYAnchor],
[title.leadingAnchor constraintEqualToAnchor:headerButton.leadingAnchor constant:16],
[arrow.centerYAnchor constraintEqualToAnchor:title.centerYAnchor],
[arrow.trailingAnchor constraintEqualToAnchor:title.trailingAnchor constant:20],
[arrow.widthAnchor constraintEqualToConstant:12],
[arrow.heightAnchor constraintEqualToConstant:18],
]];
// Vertical stack for cards
UIStackView *list = [[UIStackView alloc] init];
list.axis = UILayoutConstraintAxisVertical;
list.spacing = 12;
list.translatesAutoresizingMaskIntoConstraints = NO;
list.layoutMargins = UIEdgeInsetsMake(10, 16, 16, 16); // consistent padding
list.layoutMarginsRelativeArrangement = YES;
list.alignment = UIStackViewAlignmentFill; // fill width
[section addSubview:list];
[NSLayoutConstraint activateConstraints:@[
[list.topAnchor constraintEqualToAnchor:title.bottomAnchor],
[list.leadingAnchor constraintEqualToAnchor:section.leadingAnchor],
[list.trailingAnchor constraintEqualToAnchor:section.trailingAnchor],
[list.bottomAnchor constraintEqualToAnchor:section.bottomAnchor]
]];
for (NSDictionary *item in issues) {
UIView *card = [self makeIssueCard:item];
[list addArrangedSubview:card];
}
return section;
}
- (UIView *)makeIssueCard:(NSDictionary *)item {
UIView *card = [[UIView alloc] init];
card.translatesAutoresizingMaskIntoConstraints = NO;
card.backgroundColor = UIColor.whiteColor; card.backgroundColor = UIColor.whiteColor;
card.layer.cornerRadius = 8; card.layer.cornerRadius = 8;
card.layer.shadowColor = [UIColor colorWithWhite:0 alpha:0.1].CGColor; card.layer.shadowColor = [UIColor colorWithWhite:0 alpha:0.1].CGColor;
card.layer.shadowOpacity = 0.3; card.layer.shadowOpacity = 0.3;
card.layer.shadowOffset = CGSizeMake(0, 1); card.layer.shadowOffset = CGSizeMake(0, 1);
card.layer.masksToBounds = NO;
// === Extract fields === [card.heightAnchor constraintGreaterThanOrEqualToConstant:110].active = YES;
NSString *status = item[@"status_external"] ?: @"";
NSString *issueRef = item[@"issue_reference"] ?: @"";
NSString *createdAt = item[@"created_at"] ?: @"";
NSString *formattedDate = [self formatDate:createdAt];
UIColor *statusColor = [self colorForStatus:status];
// === Determine issue image (like renderOnlineImage) === NSString *status = item[@"status_external"] ?: @"Unknown";
NSString *issueImage = item[@"image"]; NSString *formattedDate = [self formatDate:item[@"created_at"]];
if (!issueImage || [issueImage isEqualToString:@""]) { UIColor *statusColor = [self colorForStatus:status];
NSArray *firstArray = item[@"first"];
NSArray *lastArray = item[@"last"];
if (firstArray.count > 0 && firstArray[0][@"image"])
issueImage = firstArray[0][@"image"];
else if (lastArray.count > 0 && lastArray[0][@"image"])
issueImage = lastArray[0][@"image"];
}
// === Top row: Status + Date === UIView *dot = [[UIView alloc] init];
CGFloat iconSize = 16; dot.translatesAutoresizingMaskIntoConstraints = NO;
UIView *statusDot = [[UIView alloc] initWithFrame:CGRectMake(12, 12, iconSize, iconSize)]; dot.backgroundColor = statusColor;
statusDot.layer.cornerRadius = iconSize / 2; dot.layer.cornerRadius = 8;
statusDot.backgroundColor = statusColor; [card addSubview:dot];
[card addSubview:statusDot];
UILabel *statusLabel = [[UILabel alloc] initWithFrame:CGRectMake(CGRectGetMaxX(statusDot.frame) + 6, 8, width - 160, 22)]; UILabel *statusLabel = [[UILabel alloc] init];
statusLabel.text = status.length ? status : @"Unknown"; statusLabel.translatesAutoresizingMaskIntoConstraints = NO;
statusLabel.textColor = statusColor; statusLabel.text = status;
statusLabel.font = [UIFont boldSystemFontOfSize:15]; statusLabel.font = [UIFont boldSystemFontOfSize:15];
statusLabel.textColor = statusColor;
[card addSubview:statusLabel]; [card addSubview:statusLabel];
UILabel *dateLabel = [[UILabel alloc] initWithFrame:CGRectMake(width - 130, 10, 118, 18)]; UILabel *dateLabel = [[UILabel alloc] init];
dateLabel.textAlignment = NSTextAlignmentRight; dateLabel.translatesAutoresizingMaskIntoConstraints = NO;
dateLabel.text = formattedDate; dateLabel.text = formattedDate;
dateLabel.textColor = [UIColor grayColor];
dateLabel.font = [UIFont systemFontOfSize:13]; dateLabel.font = [UIFont systemFontOfSize:13];
dateLabel.textColor = UIColor.grayColor;
dateLabel.textAlignment = NSTextAlignmentRight;
[card addSubview:dateLabel]; [card addSubview:dateLabel];
// === Divider line ===
// UIView *divider = [[UIView alloc] initWithFrame:CGRectMake(12, 36, width - 24, 0.5)];
// divider.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1];
// [card addSubview:divider];
// === Image + Info Row ===
CGFloat imageSize = 44; CGFloat imageSize = 44;
UIImageView *thumbView = [[UIImageView alloc] initWithFrame:CGRectMake(12, 48, imageSize, imageSize)]; UIImageView *thumb = [[UIImageView alloc] init];
thumbView.layer.cornerRadius = 6; thumb.translatesAutoresizingMaskIntoConstraints = NO;
thumbView.clipsToBounds = YES; thumb.layer.cornerRadius = 6;
thumbView.contentMode = UIViewContentModeScaleAspectFill; thumb.clipsToBounds = YES;
thumb.contentMode = UIViewContentModeScaleAspectFill;
thumb.image = [UIImage imageNamed:@"small_placeholder"]; // default
// Determine issue image
NSString *issueImage = item[@"image"];
if (!issueImage || [issueImage isEqualToString:@""]) {
NSArray *firstArray = item[@"first"];
NSArray *lastArray = item[@"last"];
if (firstArray.count > 0 && firstArray[0][@"image"])
issueImage = firstArray[0][@"image"];
else if (lastArray.count > 0 && lastArray[0][@"image"])
issueImage = lastArray[0][@"image"];
}
if (issueImage && issueImage.length > 0) { if (issueImage && issueImage.length > 0) {
NSURL *url = [NSURL URLWithString:issueImage]; NSURL *url = [NSURL URLWithString:issueImage];
if (url) {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:url]; NSData *data = [NSData dataWithContentsOfURL:url];
if (data) { if (data) {
thumbView.image = [UIImage imageWithData:data]; UIImage *img = [UIImage imageWithData:data];
} else { if (img) {
thumbView.image = [UIImage imageNamed:@"small_placeholder"]; dispatch_async(dispatch_get_main_queue(), ^{
thumb.image = img;
});
}
}
});
} }
} else {
thumbView.image = [UIImage imageNamed:@"small_placeholder"];
} }
[card addSubview:thumbView]; [card addSubview:thumb];
// === Text beside image ===
CGFloat textX = CGRectGetMaxX(thumbView.frame) + 10;
UILabel *infoLabel = [[UILabel alloc] initWithFrame:CGRectMake(textX, 46, width - textX - 12, 50)];
infoLabel.numberOfLines = 3;
infoLabel.textColor = [UIColor blackColor];
infoLabel.font = [UIFont systemFontOfSize:15];
infoLabel.attributedText = [self notificationTextForIssue:item];
[card addSubview:infoLabel]; UILabel *info = [[UILabel alloc] init];
info.translatesAutoresizingMaskIntoConstraints = NO;
info.numberOfLines = 3;
info.font = [UIFont systemFontOfSize:15];
info.attributedText = [self notificationTextForIssue:item];
[card addSubview:info];
// Keep a reference to its issue data [NSLayoutConstraint activateConstraints:@[
card.accessibilityValue = item[@"issue_reference"]; // for debugging [dot.leadingAnchor constraintEqualToAnchor:card.leadingAnchor constant:12],
card.tag = self.issues.count; // optional index tagging [dot.topAnchor constraintEqualToAnchor:card.topAnchor constant:12],
[dot.widthAnchor constraintEqualToConstant:16],
[dot.heightAnchor constraintEqualToConstant:16],
[statusLabel.leadingAnchor constraintEqualToAnchor:dot.trailingAnchor constant:6],
[statusLabel.centerYAnchor constraintEqualToAnchor:dot.centerYAnchor],
[dateLabel.trailingAnchor constraintEqualToAnchor:card.trailingAnchor constant:-12],
[dateLabel.centerYAnchor constraintEqualToAnchor:dot.centerYAnchor],
[thumb.leadingAnchor constraintEqualToAnchor:card.leadingAnchor constant:12],
[thumb.topAnchor constraintEqualToAnchor:dot.bottomAnchor constant:12],
[thumb.widthAnchor constraintEqualToConstant:imageSize],
[thumb.heightAnchor constraintEqualToConstant:imageSize],
[info.leadingAnchor constraintEqualToAnchor:thumb.trailingAnchor constant:10],
[info.trailingAnchor constraintEqualToAnchor:card.trailingAnchor constant:-12],
[info.centerYAnchor constraintEqualToAnchor:thumb.centerYAnchor],
[info.bottomAnchor constraintLessThanOrEqualToAnchor:card.bottomAnchor constant:-10]
]];
// Store the data for later use (simplest way)
objc_setAssociatedObject(card, "issueData", item, OBJC_ASSOCIATION_RETAIN_NONATOMIC); objc_setAssociatedObject(card, "issueData", item, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// === Tap gesture === UITapGestureRecognizer *tap =
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleIssueTap:)]; [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleIssueTap:)];
[card addGestureRecognizer:tap]; [card addGestureRecognizer:tap];
return card; return card;
......
...@@ -541,7 +541,7 @@ typedef struct { ...@@ -541,7 +541,7 @@ typedef struct {
#pragma mark - api #pragma mark - api
- (void)requestGeneralInfo { - (void)requestGeneralInfo {
[APIClient requestGeneralInfo:^(NSDictionary *data, NSError *error) { [APIClient requestGeneralInfo:^(BOOL success, NSDictionary *data, NSError *error) {
if (error || !data) { if (error || !data) {
NSLog(@"❌ General info error: %@", error.localizedDescription); NSLog(@"❌ General info error: %@", error.localizedDescription);
return; return;
......
...@@ -156,33 +156,48 @@ ...@@ -156,33 +156,48 @@
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView - (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath { cellForItemAtIndexPath:(NSIndexPath *)indexPath {
ProjectCardCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"ProjectCardCell" ProjectCardCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"ProjectCardCell"
forIndexPath:indexPath]; forIndexPath:indexPath];
// Default placeholder
cell.imageView.image = [UIImage systemImageNamed:@"photo"]; cell.imageView.image = [UIImage systemImageNamed:@"photo"];
NSDictionary *project = self.projects[indexPath.item]; NSDictionary *project = self.projects[indexPath.item];
// Project name // Project name
cell.nameLabel.text = project[@"project_name"]; id projectName = project[@"project_name"];
cell.nameLabel.text = [projectName isKindOfClass:[NSString class]] ? projectName : @"";
// Get logo URL safely
NSString *logo = project[@"logo"];
NSString *thumb = project[@"logo_thumbnail"];
NSString *logoURLString = (logo.length > 0) ? logo :
(thumb.length > 0) ? thumb : nil;
// Project logo
NSString *logoURLString = project[@"logo"] ?: project[@"logo_thumbnail"];
NSURL *url = [NSURL URLWithString:logoURLString];
// Async load image (simple placeholder approach) NSURL *url = [NSURL URLWithString:logoURLString ?: @""];
NSIndexPath *currentIndexPath = indexPath;
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ // Async load image safely
NSData *data = [NSData dataWithContentsOfURL:url]; if (url) {
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url
completionHandler:^(NSData * _Nullable data,
NSURLResponse * _Nullable response,
NSError * _Nullable error) {
UIImage *image = data ? [UIImage imageWithData:data] : nil; UIImage *image = data ? [UIImage imageWithData:data] : nil;
if (!image) return; // leave placeholder
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
NSIndexPath *visibleIndexPath = [collectionView indexPathForCell:cell]; // Only update if the cell is still visible at this indexPath
if (visibleIndexPath && visibleIndexPath.item == currentIndexPath.item) { ProjectCardCell *updateCell = (ProjectCardCell *)[collectionView cellForItemAtIndexPath:indexPath];
cell.imageView.image = image ?: [UIImage systemImageNamed:@"photo"]; if (updateCell) {
updateCell.imageView.image = image;
} }
}); });
}); }];
[task resume];
}
return cell; return cell;
} }
...@@ -194,6 +209,7 @@ ...@@ -194,6 +209,7 @@
vc.projectName = project[@"project_name"]; vc.projectName = project[@"project_name"];
vc.units = project[@"unit"]; vc.units = project[@"unit"];
vc.drawerController = self.drawerController; vc.drawerController = self.drawerController;
vc.projectLogo = project[@"logo"] ?: project[@"logo_thumbnail"];
vc.modalPresentationStyle = UIModalPresentationFullScreen; vc.modalPresentationStyle = UIModalPresentationFullScreen;
[self.navigationController pushViewController:vc animated:YES]; [self.navigationController pushViewController:vc animated:YES];
...@@ -212,7 +228,7 @@ ...@@ -212,7 +228,7 @@
#pragma mark - API #pragma mark - API
- (void)requestProjectList { - (void)requestProjectList {
[APIClient requestProjectList:^(NSDictionary *data, NSError *error) { [APIClient requestProjectList:^(BOOL success, NSDictionary *data, NSError *error) {
// debug // debug
NSString *plainText = [NSString stringWithFormat:@"%@", data]; NSString *plainText = [NSString stringWithFormat:@"%@", data];
...@@ -259,7 +275,7 @@ ...@@ -259,7 +275,7 @@
} }
- (void)requestCompanyCode { - (void)requestCompanyCode {
[APIClient requestCompanyCode:@"kita" completion:^(NSDictionary *data, NSError *error) { [APIClient requestCompanyCode:@"kita" completion:^(BOOL success, NSDictionary *data, NSError *error) {
// debug // debug
NSString *plainText = [NSString stringWithFormat:@"%@", data]; NSString *plainText = [NSString stringWithFormat:@"%@", data];
......
...@@ -13,5 +13,6 @@ ...@@ -13,5 +13,6 @@
@property (nonatomic, copy) NSString *projectId; @property (nonatomic, copy) NSString *projectId;
@property (nonatomic, copy) NSString *projectName; @property (nonatomic, copy) NSString *projectName;
@property (nonatomic, copy) NSArray *units; @property (nonatomic, copy) NSArray *units;
@property (nonatomic, copy) NSString *projectLogo;
@end @end
...@@ -260,13 +260,14 @@ didSelectRowAtIndexPath:(NSIndexPath *)indexPath { ...@@ -260,13 +260,14 @@ didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *projectId = [unit[@"project_id"] description]; NSString *projectId = [unit[@"project_id"] description];
NSString *drawingPlanId = [unit[@"id"] description]; NSString *drawingPlanId = [unit[@"id"] description];
NSLog(@"self.projectLogo: %@", self.projectLogo);
DashboardViewController *vc = [[DashboardViewController alloc] init]; DashboardViewController *vc = [[DashboardViewController alloc] init];
vc.projectCode = unit[@"name"] ?: @""; vc.projectCode = unit[@"name"] ?: @"";
vc.projectName = self.projectName; vc.projectName = self.projectName;
vc.projectId = projectId ?: @""; vc.projectId = projectId ?: @"";
vc.drawingPlanId = drawingPlanId ?: @""; vc.drawingPlanId = drawingPlanId ?: @"";
vc.drawerController = self.drawerController; vc.drawerController = self.drawerController;
vc.projectLogo = self.projectLogo;
vc.modalPresentationStyle = UIModalPresentationFullScreen; vc.modalPresentationStyle = UIModalPresentationFullScreen;
[self.navigationController pushViewController:vc animated:YES]; [self.navigationController pushViewController:vc animated:YES];
} }
......
...@@ -597,7 +597,7 @@ didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey,id> * ...@@ -597,7 +597,7 @@ didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey,id> *
// ✅ JSON ONLY // ✅ JSON ONLY
[APIClient submitAddInfo:self.issueID [APIClient submitAddInfo:self.issueID
remarks:self.commentTextView.text remarks:self.commentTextView.text
completion:^(NSDictionary *response, NSError *error) { completion:^(BOOL success, NSDictionary *response, NSError *error) {
[self handleSubmitResponse:response error:error]; [self handleSubmitResponse:response error:error];
}]; }];
return; return;
...@@ -607,7 +607,7 @@ didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey,id> * ...@@ -607,7 +607,7 @@ didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey,id> *
images:images images:images
mediaURL:self.selectedMediaURL mediaURL:self.selectedMediaURL
mediaType:mediaType mediaType:mediaType
completion:^(NSDictionary *response, NSError *error) { completion:^(BOOL success, NSDictionary *response, NSError *error) {
[self handleSubmitResponse:response error:error]; [self handleSubmitResponse:response error:error];
}]; }];
} }
......
...@@ -957,7 +957,7 @@ didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey,id> * ...@@ -957,7 +957,7 @@ didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey,id> *
[APIClient submitUpdateIssue:updateData [APIClient submitUpdateIssue:updateData
images:self.addedImages images:self.addedImages
completion:^(NSDictionary * _Nullable response, NSError * _Nullable error) { completion:^(BOOL success, NSDictionary *response, NSError *error){
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (error) { if (error) {
[self showAlertWithTitle:@"Update Failed" message:error.localizedDescription]; [self showAlertWithTitle:@"Update Failed" message:error.localizedDescription];
...@@ -1043,7 +1043,7 @@ didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey,id> * ...@@ -1043,7 +1043,7 @@ didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey,id> *
[APIClient submitAddIssue:issueData [APIClient submitAddIssue:issueData
images:self.uploadedImages images:self.uploadedImages
completion:^(NSDictionary *response, NSError *error) { completion:^(BOOL success, NSDictionary *response, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (error) { if (error) {
NSLog(@"❌ Submission failed: %@", error.localizedDescription); NSLog(@"❌ Submission failed: %@", error.localizedDescription);
......
...@@ -69,7 +69,7 @@ ...@@ -69,7 +69,7 @@
- (void)handleGetUnitPlan { - (void)handleGetUnitPlan {
NSLog(@"🌐 requesting Unit Plan data..."); NSLog(@"🌐 requesting Unit Plan data...");
[APIClient requestGetUnitPlan:^(NSDictionary *data, NSError *error) { [APIClient requestGetUnitPlan:^(BOOL success, NSDictionary *data, NSError *error) {
if (error) { if (error) {
NSLog(@"❌ Unit Plan request failed: %@", error.localizedDescription); NSLog(@"❌ Unit Plan request failed: %@", error.localizedDescription);
return; return;
...@@ -710,7 +710,7 @@ ...@@ -710,7 +710,7 @@
- (void)handleSettingsByLocation:(NSString *)locationID planID:(NSString *)planID { - (void)handleSettingsByLocation:(NSString *)locationID planID:(NSString *)planID {
[APIClient requestSettingsByLocation:locationID [APIClient requestSettingsByLocation:locationID
completion:^(NSDictionary *data, NSError *error) { completion:^(BOOL success, NSDictionary *data, NSError *error) {
if (error) { if (error) {
NSLog(@"❌ Error requesting settings: %@", error); NSLog(@"❌ Error requesting settings: %@", error);
return; return;
......
...@@ -127,7 +127,7 @@ ...@@ -127,7 +127,7 @@
#pragma mark - API request/response #pragma mark - API request/response
- (void)handleIssues { - (void)handleIssues {
[APIClient requestHistory:self.issueID [APIClient requestHistory:self.issueID
completion:^(NSDictionary *data, NSError *error) { completion:^(BOOL success, NSDictionary *data, NSError *error) {
if (error) { if (error) {
NSLog(@"❌ Error requesting history: %@", error); NSLog(@"❌ Error requesting history: %@", error);
return; return;
......
...@@ -300,7 +300,7 @@ ...@@ -300,7 +300,7 @@
- (void)handleGetIssue { - (void)handleGetIssue {
NSLog(@"🌐 requesting Issue data..."); NSLog(@"🌐 requesting Issue data...");
[APIClient requestIssues:^(NSDictionary *data, NSError *error) { [APIClient requestIssues:^(BOOL success, NSDictionary *data, NSError *error) {
if (error) { if (error) {
NSLog(@"❌ Issue request failed: %@", error.localizedDescription); NSLog(@"❌ Issue request failed: %@", error.localizedDescription);
return; return;
......
...@@ -286,7 +286,7 @@ ...@@ -286,7 +286,7 @@
[searchContainer.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant:50], [searchContainer.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant:50],
[searchContainer.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:8], [searchContainer.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:8],
[searchContainer.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-8], [searchContainer.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-8],
[searchContainer.heightAnchor constraintEqualToConstant:100], [searchContainer.heightAnchor constraintEqualToConstant:120],
[self.searchBar.leadingAnchor constraintEqualToAnchor:searchContainer.leadingAnchor], [self.searchBar.leadingAnchor constraintEqualToAnchor:searchContainer.leadingAnchor],
[self.searchBar.trailingAnchor constraintEqualToAnchor:searchContainer.trailingAnchor], [self.searchBar.trailingAnchor constraintEqualToAnchor:searchContainer.trailingAnchor],
...@@ -383,7 +383,7 @@ ...@@ -383,7 +383,7 @@
#pragma mark - Data #pragma mark - Data
- (void)requestIssues { - (void)requestIssues {
[APIClient requestIssues:^(NSDictionary *data, NSError *error) { [APIClient requestIssues:^(BOOL success, NSDictionary *data, NSError *error) {
if (error) return; if (error) return;
NSArray *allIssues = data[@"Data"] ?: @[]; NSArray *allIssues = data[@"Data"] ?: @[];
......
...@@ -264,7 +264,7 @@ ...@@ -264,7 +264,7 @@
- (void)requestDeleteIssue:(NSString *)issueID remarks:(NSString *)remarks { - (void)requestDeleteIssue:(NSString *)issueID remarks:(NSString *)remarks {
NSLog(@"🚀 Sending DELETE ISSUE for ID=%@, remarks=%@", issueID, remarks); NSLog(@"🚀 Sending DELETE ISSUE for ID=%@, remarks=%@", issueID, remarks);
[APIClient requestDeleteIssue:issueID remarks:remarks completion:^(NSDictionary *data, NSError *error) { [APIClient requestDeleteIssue:issueID remarks:remarks completion:^(BOOL success, NSDictionary *data, NSError *error) {
if (error) { if (error) {
NSLog(@"Error %@", error.localizedDescription); NSLog(@"Error %@", error.localizedDescription);
return; return;
...@@ -303,7 +303,7 @@ ...@@ -303,7 +303,7 @@
[APIClient requestVoidIssue:ids [APIClient requestVoidIssue:ids
remarks:remarks remarks:remarks
completion:^(NSDictionary *data, NSError *error) { completion:^(BOOL success, NSDictionary *data, NSError *error) {
if (error) { if (error) {
NSLog(@"Error %@", error.localizedDescription); NSLog(@"Error %@", error.localizedDescription);
return; return;
......
...@@ -105,6 +105,7 @@ UICollectionViewDataSource ...@@ -105,6 +105,7 @@ UICollectionViewDataSource
@implementation SyncProjectViewController @implementation SyncProjectViewController
- (void)viewDidLoad { - (void)viewDidLoad {
[super viewDidLoad]; [super viewDidLoad];
self.view.backgroundColor = [UIColor colorWithWhite:0.96 alpha:1.0];
self.projects = [NSMutableArray array]; self.projects = [NSMutableArray array];
self.selectedUnits = [NSMutableArray array]; self.selectedUnits = [NSMutableArray array];
...@@ -480,7 +481,7 @@ didSelectItemAtIndexPath:(NSIndexPath *)indexPath { ...@@ -480,7 +481,7 @@ didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
[self showLoading:YES]; [self showLoading:YES];
[APIClient requestProjectList:^(NSDictionary *data, NSError *error) { [APIClient requestProjectList:^(BOOL success, NSDictionary *data, NSError *error) {
// debug // debug
NSString *plainText = [NSString stringWithFormat:@"%@", data]; NSString *plainText = [NSString stringWithFormat:@"%@", data];
......
...@@ -8,22 +8,22 @@ NS_ASSUME_NONNULL_BEGIN ...@@ -8,22 +8,22 @@ NS_ASSUME_NONNULL_BEGIN
+ (void)requestAccessItem:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion; + (void)requestAccessItem:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion;
+ (void)requestGetUnitPlan:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion; + (void)requestGetUnitPlan:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * error))completion;
+ (void)requestSettingsByLocation:(NSString * _Nonnull)locationID + (void)requestSettingsByLocation:(NSString * _Nonnull)locationID
completion:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion; completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * error))completion;
+ (void)submitAddIssue:(NSDictionary * _Nonnull)params + (void)submitAddIssue:(NSDictionary * _Nonnull)params
images:(NSArray<UIImage *> * _Nullable)images images:(NSArray<UIImage *> * _Nullable)images
completion:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion; completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * error))completion;
+ (void)submitUpdateIssue:(NSDictionary * _Nonnull)updateData + (void)submitUpdateIssue:(NSDictionary * _Nonnull)updateData
images:(NSArray<UIImage *> * _Nullable)images images:(NSArray<UIImage *> * _Nullable)images
completion:(void (^)(NSDictionary * _Nullable response, NSError * _Nullable error))completion; completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion;
+ (void)requestAppointment:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion; + (void)requestAppointment:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * error))completion;
+ (void)requestGeneralInfo:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion; + (void)requestGeneralInfo:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * error))completion;
+ (void)submitAppointment:(NSDictionary *)appointmentData + (void)submitAppointment:(NSDictionary *)appointmentData
completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion; completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion;
...@@ -33,52 +33,52 @@ NS_ASSUME_NONNULL_BEGIN ...@@ -33,52 +33,52 @@ NS_ASSUME_NONNULL_BEGIN
+ (void)requestCancelAppointment:(id)appointmentId + (void)requestCancelAppointment:(id)appointmentId
reason:(NSString *)reason reason:(NSString *)reason
completion:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion; completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * error))completion;
+ (void)requestDashboardInfo:(void (^)(NSDictionary *data, NSError *error))completion; + (void)requestDashboardInfo:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * error))completion;
+ (void)requestAnnouncement:(void (^)(NSDictionary *data, NSError *error))completion; + (void)requestAnnouncement:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * error))completion;
+ (void)requestHistory:(NSString *)issueID + (void)requestHistory:(NSString *)issueID
completion:(void (^)(NSDictionary *data, NSError *error))completion; completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * error))completion;
+(void)submitAddInfo:(NSString *)issueID +(void)submitAddInfo:(NSString *)issueID
remarks:(NSString *)remarks remarks:(NSString *)remarks
completion:(void (^)(NSDictionary *data, NSError *error))completion; completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * error))completion;
+ (void)uploadAddInfo:(NSString *)issueID + (void)uploadAddInfo:(NSString *)issueID
remarks:(NSString *)remarks remarks:(NSString *)remarks
images:(NSArray<UIImage *> *)images images:(NSArray<UIImage *> *)images
mediaURL:(NSURL *)mediaURL mediaURL:(NSURL *)mediaURL
mediaType:(NSString *)mediaType mediaType:(NSString *)mediaType
completion:(void (^)(NSDictionary *data, NSError *error))completion; completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * error))completion;
+ (void)requestIssues:(void (^)(NSDictionary *data, NSError *error))completion; + (void)requestIssues:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * error))completion;
+ (void)requestVoidIssue:(id)issueID + (void)requestVoidIssue:(id)issueID
remarks:(NSString *)remarks remarks:(NSString *)remarks
completion:(void (^)(NSDictionary *data, NSError *error))completion; completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * error))completion;
+ (void)requestDeleteIssue:(id)issueID + (void)requestDeleteIssue:(id)issueID
remarks:(NSString *)remarks remarks:(NSString *)remarks
completion:(void (^)(NSDictionary *data, NSError *error))completion; completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * error))completion;
+ (void)requestCommonArea:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion; + (void)requestCommonArea:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * error))completion;
+ (void)requestGeneralSettings:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion; + (void)requestGeneralSettings:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * error))completion;
+ (void)requestDefectMatrix:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion; + (void)requestDefectMatrix:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * error))completion;
+ (void)requestProjectList:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion; + (void)requestProjectList:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * error))completion;
+ (void)requestArrayImage:(NSArray *)imageArray + (void)requestArrayImage:(NSArray *)imageArray
completion:(void (^)(NSDictionary *data, NSError *error))completion; completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * error))completion;
+ (void)downloadAndCacheImage:(NSURL *)url + (void)downloadAndCacheImage:(NSURL *)url
completion:(void (^)(BOOL success))completion; completion:(void (^)(BOOL success, NSString *filePath))completion;
+ (void)requestCompanyCode:(NSString *)companyCode + (void)requestCompanyCode:(NSString *)companyCode
completion:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion; completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * error))completion;
@end @end
......
// APIClient.mm // APIClient.mm
#import "APIClient.h" #import "APIClient.h"
#import "APIConfig.h" #import "APIConfig.h"
#import <CommonCrypto/CommonDigest.h>
#import "LocalStorage.h"
@implementation APIClient @implementation APIClient
+ (void)requestAccessItem:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion { + (void)requestAccessItem:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion {
if ([APIConfig handleOfflineForAPI:@"AccessItem" completion:completion]) {
return;
}
NSURL *url = [APIConfig urlWithPath:@"/owner/plan/getAccessItem"]; NSURL *url = [APIConfig urlWithPath:@"/owner/plan/getAccessItem"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
...@@ -59,8 +65,10 @@ ...@@ -59,8 +65,10 @@
return; return;
} }
NSLog(@"✅ SUBMIT (NO REP) API Response:\n%@", json); NSString *cacheKey = [NSString stringWithFormat:@"AccessItem_%@", [APIConfig projectId]];
NSDictionary *safeJson = [APIConfig dictionaryByReplacingNullsWithBlanks:json];
[LocalStorage saveDictionary:safeJson forKey:cacheKey];
NSLog(@"💾 Saved to cache: %@", cacheKey);
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
completion(YES, json, nil); completion(YES, json, nil);
}); });
...@@ -69,7 +77,11 @@ ...@@ -69,7 +77,11 @@
[task resume]; [task resume];
} }
+ (void)requestGetUnitPlan:(void (^)(NSDictionary *data, NSError *error))completion { + (void)requestGetUnitPlan:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion {
if ([APIConfig handleOfflineForAPI:@"GetUnitPlan" completion:completion]) {
return;
}
// ✅ Construct URL (with token) // ✅ Construct URL (with token)
NSURL *url = [APIConfig urlWithPath:@"/owner/plan/getLocationByUnit"]; NSURL *url = [APIConfig urlWithPath:@"/owner/plan/getLocationByUnit"];
...@@ -119,7 +131,7 @@ ...@@ -119,7 +131,7 @@
if (error) { if (error) {
NSLog(@"❌ [requestUnitPlan] Network error: %@", error); NSLog(@"❌ [requestUnitPlan] Network error: %@", error);
if (completion) completion(nil, error); if (completion) completion(NO, nil, error);
return; return;
} }
...@@ -128,7 +140,7 @@ ...@@ -128,7 +140,7 @@
if (jsonErr) { if (jsonErr) {
NSLog(@"❌ [requestUnitPlan] JSON parse error: %@", jsonErr); NSLog(@"❌ [requestUnitPlan] JSON parse error: %@", jsonErr);
if (completion) completion(nil, jsonErr); if (completion) completion(NO, nil, jsonErr);
return; return;
} }
...@@ -145,8 +157,12 @@ ...@@ -145,8 +157,12 @@
// ✅ Call back on main thread // ✅ Call back on main thread
if (completion) { if (completion) {
NSString *cacheKey = [NSString stringWithFormat:@"GetUnitPlan_%@", [APIConfig projectId]];
NSDictionary *safeJson = [APIConfig dictionaryByReplacingNullsWithBlanks:json];
[LocalStorage saveDictionary:safeJson forKey:cacheKey];
NSLog(@"💾 Saved to cache: %@", cacheKey);
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
completion(json, nil); completion(YES, json, nil);
}); });
} }
}]; }];
...@@ -155,9 +171,13 @@ ...@@ -155,9 +171,13 @@
} }
+ (void)requestSettingsByLocation:(NSString *)locationID + (void)requestSettingsByLocation:(NSString *)locationID
completion:(void (^)(NSDictionary *data, NSError *error))completion { completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion {
if ([APIConfig handleOfflineForAPI:@"SettingsByLocation" completion:completion]) {
return;
}
NSURL *url = [APIConfig urlWithPath:@"/owner/plan/getGeneralSettingByLocation"]; NSURL *url = [APIConfig urlWithPath:@"/issue/getGeneralSettingByLocation"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST"; request.HTTPMethod = @"POST";
...@@ -204,7 +224,7 @@ ...@@ -204,7 +224,7 @@
if (error) { if (error) {
NSLog(@"❌ [requestSettingsByLocation] Network error: %@", error); NSLog(@"❌ [requestSettingsByLocation] Network error: %@", error);
if (completion) completion(nil, error); if (completion) completion(NO, nil, error);
return; return;
} }
...@@ -212,7 +232,7 @@ ...@@ -212,7 +232,7 @@
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonErr]; NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonErr];
if (jsonErr) { if (jsonErr) {
NSLog(@"❌ JSON parse error: %@", jsonErr); NSLog(@"❌ JSON parse error: %@", jsonErr);
if (completion) completion(nil, jsonErr); if (completion) completion(NO, nil, jsonErr);
return; return;
} }
...@@ -232,8 +252,12 @@ ...@@ -232,8 +252,12 @@
} }
if (completion) { if (completion) {
NSString *cacheKey = [NSString stringWithFormat:@"SettingsByLocation_%@", [APIConfig projectId]];
NSDictionary *safeJson = [APIConfig dictionaryByReplacingNullsWithBlanks:json];
[LocalStorage saveDictionary:safeJson forKey:cacheKey];
NSLog(@"💾 Saved to cache: %@", cacheKey);
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
completion(json, nil); completion(YES, json, nil);
}); });
} }
}]; }];
...@@ -243,7 +267,7 @@ ...@@ -243,7 +267,7 @@
+ (void)submitAddIssue:(NSDictionary *)issueData + (void)submitAddIssue:(NSDictionary *)issueData
images:(NSArray<UIImage *> * _Nullable)images images:(NSArray<UIImage *> * _Nullable)images
completion:(void (^)(NSDictionary * _Nullable response, NSError * _Nullable error))completion { completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion {
NSURL *url = [APIConfig urlWithPath:@"/owner/issue/addIssue"]; NSURL *url = [APIConfig urlWithPath:@"/owner/issue/addIssue"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
...@@ -313,7 +337,7 @@ ...@@ -313,7 +337,7 @@
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) { if (error) {
NSLog(@"❌ [AddIssue] Network error: %@", error); NSLog(@"❌ [AddIssue] Network error: %@", error);
dispatch_async(dispatch_get_main_queue(), ^{ if (completion) completion(nil, error); }); dispatch_async(dispatch_get_main_queue(), ^{ if (completion) completion(NO, nil, error); });
return; return;
} }
...@@ -322,12 +346,12 @@ ...@@ -322,12 +346,12 @@
if (jsonErr) { if (jsonErr) {
NSString *raw = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; NSString *raw = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"⚠️ [AddIssue] JSON parse error: %@\nRaw response: %@", jsonErr, raw); NSLog(@"⚠️ [AddIssue] JSON parse error: %@\nRaw response: %@", jsonErr, raw);
dispatch_async(dispatch_get_main_queue(), ^{ if (completion) completion(nil, jsonErr); }); dispatch_async(dispatch_get_main_queue(), ^{ if (completion) completion(NO, nil, jsonErr); });
return; return;
} }
NSLog(@"✅ [AddIssue] Response: %@", json); NSLog(@"✅ [AddIssue] Response: %@", json);
dispatch_async(dispatch_get_main_queue(), ^{ if (completion) completion(json, nil); }); dispatch_async(dispatch_get_main_queue(), ^{ if (completion) completion(YES, json, nil); });
}]; }];
[task resume]; [task resume];
...@@ -335,7 +359,7 @@ ...@@ -335,7 +359,7 @@
+ (void)submitUpdateIssue:(NSDictionary *)updateData + (void)submitUpdateIssue:(NSDictionary *)updateData
images:(NSArray<UIImage *> * _Nullable)images images:(NSArray<UIImage *> * _Nullable)images
completion:(void (^)(NSDictionary * _Nullable response, NSError * _Nullable error))completion { completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion{
NSURL *url = [APIConfig urlWithPath:@"/owner/issue/updateIssue"]; NSURL *url = [APIConfig urlWithPath:@"/owner/issue/updateIssue"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
...@@ -415,7 +439,7 @@ ...@@ -415,7 +439,7 @@
if (error) { if (error) {
NSLog(@"❌ [UpdateIssue] Network error: %@", error); NSLog(@"❌ [UpdateIssue] Network error: %@", error);
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(nil, error); if (completion) completion(NO, nil, error);
}); });
return; return;
} }
...@@ -426,20 +450,25 @@ ...@@ -426,20 +450,25 @@
NSString *raw = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; NSString *raw = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"⚠️ [UpdateIssue] JSON parse error: %@\nRaw: %@", jsonErr, raw); NSLog(@"⚠️ [UpdateIssue] JSON parse error: %@\nRaw: %@", jsonErr, raw);
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(nil, jsonErr); if (completion) completion(NO, nil, jsonErr);
}); });
return; return;
} }
NSLog(@"✅ [UpdateIssue] Response: %@", json); NSLog(@"✅ [UpdateIssue] Response: %@", json);
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(json, nil); if (completion) completion(YES, json, nil);
}); });
}]; }];
[task resume]; [task resume];
} }
+ (void)requestAppointment:(void (^)(NSDictionary *data, NSError *error))completion { + (void)requestAppointment:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion {
if ([APIConfig handleOfflineForAPI:@"Appointment" completion:completion]) {
return;
}
// ✅ URL // ✅ URL
NSURL *url = [APIConfig urlWithPath:@"/owner/appointment/getSlot"]; NSURL *url = [APIConfig urlWithPath:@"/owner/appointment/getSlot"];
...@@ -464,7 +493,7 @@ ...@@ -464,7 +493,7 @@
NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError]; NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError];
if (jsonError) { if (jsonError) {
NSLog(@"❌ JSON Serialization Error: %@", jsonError); NSLog(@"❌ JSON Serialization Error: %@", jsonError);
if (completion) completion(nil, jsonError); if (completion) completion(NO, nil, jsonError);
return; return;
} }
request.HTTPBody = bodyData; request.HTTPBody = bodyData;
...@@ -481,7 +510,7 @@ ...@@ -481,7 +510,7 @@
if (error) { if (error) {
NSLog(@"❌ Network Error: %@", error); NSLog(@"❌ Network Error: %@", error);
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(nil, error); if (completion) completion(NO, nil, error);
}); });
return; return;
} }
...@@ -496,14 +525,24 @@ ...@@ -496,14 +525,24 @@
} }
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(jsonResponse, jsonParseError); if (completion) {
NSString *cacheKey = [NSString stringWithFormat:@"Appointment_%@", [APIConfig projectId]];
NSDictionary *safeJson = [APIConfig dictionaryByReplacingNullsWithBlanks:jsonResponse];
[LocalStorage saveDictionary:safeJson forKey:cacheKey];
NSLog(@"💾 Saved to cache: %@", cacheKey);
completion(YES, jsonResponse, jsonParseError);
}
}); });
}]; }];
[task resume]; [task resume];
} }
+ (void)requestGeneralInfo:(void (^)(NSDictionary *data, NSError *error))completion { + (void)requestGeneralInfo:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion {
if ([APIConfig handleOfflineForAPI:@"GeneralInfo" completion:completion]) {
return;
}
// ✅ URL // ✅ URL
NSURL *url = [APIConfig urlWithPath:@"/owner/plan/getUnitInfo"]; NSURL *url = [APIConfig urlWithPath:@"/owner/plan/getUnitInfo"];
...@@ -527,7 +566,7 @@ ...@@ -527,7 +566,7 @@
NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError]; NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError];
if (jsonError) { if (jsonError) {
NSLog(@"❌ JSON Serialization Error: %@", jsonError); NSLog(@"❌ JSON Serialization Error: %@", jsonError);
if (completion) completion(nil, jsonError); if (completion) completion(NO, nil, jsonError);
return; return;
} }
request.HTTPBody = bodyData; request.HTTPBody = bodyData;
...@@ -544,7 +583,7 @@ ...@@ -544,7 +583,7 @@
if (error) { if (error) {
NSLog(@"❌ Network Error: %@", error); NSLog(@"❌ Network Error: %@", error);
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(nil, error); if (completion) completion(NO, nil, error);
}); });
return; return;
} }
...@@ -559,7 +598,13 @@ ...@@ -559,7 +598,13 @@
} }
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(jsonResponse, jsonParseError); if (completion) {
NSString *cacheKey = [NSString stringWithFormat:@"GeneralInfo_%@", [APIConfig projectId]];
NSDictionary *safeJson = [APIConfig dictionaryByReplacingNullsWithBlanks:jsonResponse];
[LocalStorage saveDictionary:safeJson forKey:cacheKey];
NSLog(@"💾 Saved to cache: %@", cacheKey);
completion(YES, jsonResponse, jsonParseError);
}
}); });
}]; }];
...@@ -692,7 +737,11 @@ ...@@ -692,7 +737,11 @@
+ (void)requestCancelAppointment:(id)appointmentId + (void)requestCancelAppointment:(id)appointmentId
reason:(NSString *)reason reason:(NSString *)reason
completion:(void (^)(NSDictionary *data, NSError *error))completion { completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion {
if ([APIConfig handleOfflineForAPI:@"CancelAppointment" completion:completion]) {
return;
}
// ✅ URL // ✅ URL
NSURL *url = [APIConfig urlWithPath:@"/owner/appointment/cancelAppointment"]; NSURL *url = [APIConfig urlWithPath:@"/owner/appointment/cancelAppointment"];
...@@ -717,7 +766,7 @@ ...@@ -717,7 +766,7 @@
NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError]; NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError];
if (jsonError) { if (jsonError) {
NSLog(@"❌ JSON Serialization Error: %@", jsonError); NSLog(@"❌ JSON Serialization Error: %@", jsonError);
if (completion) completion(nil, jsonError); if (completion) completion(NO, nil, jsonError);
return; return;
} }
request.HTTPBody = bodyData; request.HTTPBody = bodyData;
...@@ -734,7 +783,7 @@ ...@@ -734,7 +783,7 @@
if (error) { if (error) {
NSLog(@"❌ Network Error: %@", error); NSLog(@"❌ Network Error: %@", error);
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(nil, error); if (completion) completion(NO, nil, error);
}); });
return; return;
} }
...@@ -749,14 +798,25 @@ ...@@ -749,14 +798,25 @@
} }
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(jsonResponse, jsonParseError); if (completion) {
NSString *cacheKey = [NSString stringWithFormat:@"CancelAppointment_%@", [APIConfig projectId]];
NSDictionary *safeJson = [APIConfig dictionaryByReplacingNullsWithBlanks:jsonResponse];
[LocalStorage saveDictionary:safeJson forKey:cacheKey];
NSLog(@"💾 Saved to cache: %@", cacheKey);
completion(YES, jsonResponse, jsonParseError);
}
}); });
}]; }];
[task resume]; [task resume];
} }
+ (void)requestDashboardInfo:(void (^)(NSDictionary *data, NSError *error))completion { + (void)requestDashboardInfo:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion {
if ([APIConfig handleOfflineForAPI:@"DashboardInfo" completion:completion]) {
return;
}
NSURL *url = [APIConfig urlWithPath:@"/owner/plan/getIssueUpdateAppointmentInfo"]; NSURL *url = [APIConfig urlWithPath:@"/owner/plan/getIssueUpdateAppointmentInfo"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
...@@ -796,7 +856,7 @@ ...@@ -796,7 +856,7 @@
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) { if (error) {
NSLog(@"❌ Network error: %@", error); NSLog(@"❌ Network error: %@", error);
if (completion) completion(nil, error); if (completion) completion(NO, nil, error);
return; return;
} }
...@@ -805,15 +865,23 @@ ...@@ -805,15 +865,23 @@
NSLog(@"🧩 Dashboard Info response: %@", json); NSLog(@"🧩 Dashboard Info response: %@", json);
if (completion) { if (completion) {
NSString *cacheKey = [NSString stringWithFormat:@"DashboardInfo_%@", [APIConfig projectId]];
NSDictionary *safeJson = [APIConfig dictionaryByReplacingNullsWithBlanks:json];
[LocalStorage saveDictionary:safeJson forKey:cacheKey];
NSLog(@"💾 Saved to cache: %@", cacheKey);
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
completion(json, jsonErr); completion(YES, json, jsonErr);
}); });
} }
}]; }];
[task resume]; [task resume];
} }
+ (void)requestAnnouncement:(void (^)(NSDictionary *data, NSError *error))completion { + (void)requestAnnouncement:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion {
if ([APIConfig handleOfflineForAPI:@"Announcement" completion:completion]) {
return;
}
// ✅ Keep token in URL // ✅ Keep token in URL
NSURL *url = [APIConfig urlWithPath:@"/clientAnnouncement"]; NSURL *url = [APIConfig urlWithPath:@"/clientAnnouncement"];
...@@ -854,7 +922,7 @@ ...@@ -854,7 +922,7 @@
if (error) { if (error) {
NSLog(@"❌ Network error: %@", error); NSLog(@"❌ Network error: %@", error);
if (completion) completion(nil, error); if (completion) completion(NO, nil, error);
return; return;
} }
...@@ -873,8 +941,12 @@ ...@@ -873,8 +941,12 @@
} }
if (completion) { if (completion) {
NSString *cacheKey = [NSString stringWithFormat:@"Announcement_%@", [APIConfig projectId]];
NSDictionary *safeJson = [APIConfig dictionaryByReplacingNullsWithBlanks:json];
[LocalStorage saveDictionary:safeJson forKey:cacheKey];
NSLog(@"💾 Saved to cache: %@", cacheKey);
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
completion(json, jsonErr); completion(YES, json, jsonErr);
}); });
} }
}]; }];
...@@ -882,7 +954,11 @@ ...@@ -882,7 +954,11 @@
} }
+ (void)requestHistory:(NSString *)issueID + (void)requestHistory:(NSString *)issueID
completion:(void (^)(NSDictionary *data, NSError *error))completion { completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion {
if ([APIConfig handleOfflineForAPI:@"History" completion:completion]) {
return;
}
// ✅ URL // ✅ URL
NSURL *url = [APIConfig urlWithPath:@"/owner/issue/getIssueHistory"]; NSURL *url = [APIConfig urlWithPath:@"/owner/issue/getIssueHistory"];
...@@ -906,7 +982,7 @@ ...@@ -906,7 +982,7 @@
NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError]; NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError];
if (jsonError) { if (jsonError) {
NSLog(@"❌ JSON Serialization Error: %@", jsonError); NSLog(@"❌ JSON Serialization Error: %@", jsonError);
if (completion) completion(nil, jsonError); if (completion) completion(NO, nil, jsonError);
return; return;
} }
request.HTTPBody = bodyData; request.HTTPBody = bodyData;
...@@ -923,7 +999,7 @@ ...@@ -923,7 +999,7 @@
if (error) { if (error) {
NSLog(@"❌ Network Error: %@", error); NSLog(@"❌ Network Error: %@", error);
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(nil, error); if (completion) completion(NO, nil, error);
}); });
return; return;
} }
...@@ -938,7 +1014,13 @@ ...@@ -938,7 +1014,13 @@
} }
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(jsonResponse, jsonParseError); if (completion) {
NSString *cacheKey = [NSString stringWithFormat:@"History_%@", [APIConfig projectId]];
NSDictionary *safeJson = [APIConfig dictionaryByReplacingNullsWithBlanks:jsonResponse];
[LocalStorage saveDictionary:safeJson forKey:cacheKey];
NSLog(@"💾 Saved to cache: %@", cacheKey);
completion(YES, jsonResponse, jsonParseError);
}
}); });
}]; }];
...@@ -947,7 +1029,9 @@ ...@@ -947,7 +1029,9 @@
+(void)submitAddInfo:(NSString *)issueID +(void)submitAddInfo:(NSString *)issueID
remarks:(NSString *)remarks remarks:(NSString *)remarks
completion:(void (^)(NSDictionary *data, NSError *error))completion { completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion {
// ✅ URL // ✅ URL
NSURL *url = [APIConfig urlWithPath:@"/owner/issue/addInfo"]; NSURL *url = [APIConfig urlWithPath:@"/owner/issue/addInfo"];
...@@ -973,7 +1057,7 @@ ...@@ -973,7 +1057,7 @@
NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError]; NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError];
if (jsonError) { if (jsonError) {
NSLog(@"❌ JSON Serialization Error: %@", jsonError); NSLog(@"❌ JSON Serialization Error: %@", jsonError);
if (completion) completion(nil, jsonError); if (completion) completion(NO, nil, jsonError);
return; return;
} }
request.HTTPBody = bodyData; request.HTTPBody = bodyData;
...@@ -990,7 +1074,7 @@ ...@@ -990,7 +1074,7 @@
if (error) { if (error) {
NSLog(@"❌ Network Error: %@", error); NSLog(@"❌ Network Error: %@", error);
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(nil, error); if (completion) completion(NO, nil, error);
}); });
return; return;
} }
...@@ -1005,7 +1089,7 @@ ...@@ -1005,7 +1089,7 @@
} }
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(jsonResponse, jsonParseError); if (completion) completion(YES, jsonResponse, jsonParseError);
}); });
}]; }];
...@@ -1017,7 +1101,7 @@ ...@@ -1017,7 +1101,7 @@
images:(NSArray<UIImage *> *)images images:(NSArray<UIImage *> *)images
mediaURL:(NSURL *)mediaURL mediaURL:(NSURL *)mediaURL
mediaType:(NSString *)mediaType mediaType:(NSString *)mediaType
completion:(void (^)(NSDictionary *data, NSError *error))completion { completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion {
NSURL *url = [APIConfig urlWithPath:@"/owner/issue/addInfo"]; NSURL *url = [APIConfig urlWithPath:@"/owner/issue/addInfo"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
...@@ -1096,21 +1180,19 @@ ...@@ -1096,21 +1180,19 @@
} }
} }
[body appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] [body appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary]
dataUsingEncoding:NSUTF8StringEncoding]]; dataUsingEncoding:NSUTF8StringEncoding]];
request.HTTPBody = body; request.HTTPBody = body;
// Debug // Debug
NSLog(@"📦 [AddIssue] Uploading %lu image(s)", (unsigned long)images.count); NSLog(@"📦 [AddInfo] Uploading %lu image(s)", (unsigned long)images.count);
NSURLSessionDataTask *task = [[NSURLSession sharedSession] NSURLSessionDataTask *task = [[NSURLSession sharedSession]
dataTaskWithRequest:request dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) { if (error) {
NSLog(@"❌ [AddIssue] Network error: %@", error); NSLog(@"❌ [AddInfo] Network error: %@", error);
dispatch_async(dispatch_get_main_queue(), ^{ if (completion) completion(nil, error); }); dispatch_async(dispatch_get_main_queue(), ^{ if (completion) completion(NO, nil, error); });
return; return;
} }
...@@ -1118,19 +1200,23 @@ ...@@ -1118,19 +1200,23 @@
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonErr]; NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonErr];
if (jsonErr) { if (jsonErr) {
NSString *raw = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; NSString *raw = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"⚠️ [AddIssue] JSON parse error: %@\nRaw response: %@", jsonErr, raw); NSLog(@"⚠️ [AddInfo] JSON parse error: %@\nRaw response: %@", jsonErr, raw);
dispatch_async(dispatch_get_main_queue(), ^{ if (completion) completion(nil, jsonErr); }); dispatch_async(dispatch_get_main_queue(), ^{ if (completion) completion(NO, nil, jsonErr); });
return; return;
} }
NSLog(@"✅ [AddIssue] Response: %@", json); NSLog(@"✅ [AddInfo] Response: %@", json);
dispatch_async(dispatch_get_main_queue(), ^{ if (completion) completion(json, nil); }); dispatch_async(dispatch_get_main_queue(), ^{ if (completion) completion(YES, json, nil); });
}]; }];
[task resume]; [task resume];
} }
+ (void)requestIssues:(void (^)(NSDictionary *data, NSError *error))completion { + (void)requestIssues:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion {
if ([APIConfig handleOfflineForAPI:@"Issues" completion:completion]) {
return;
}
// ✅ URL // ✅ URL
NSURL *url = [APIConfig urlWithPath:@"/owner/issue/getIssue"]; NSURL *url = [APIConfig urlWithPath:@"/owner/issue/getIssue"];
...@@ -1154,7 +1240,7 @@ ...@@ -1154,7 +1240,7 @@
NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError]; NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError];
if (jsonError) { if (jsonError) {
NSLog(@"❌ JSON Serialization Error: %@", jsonError); NSLog(@"❌ JSON Serialization Error: %@", jsonError);
if (completion) completion(nil, jsonError); if (completion) completion(NO, nil, jsonError);
return; return;
} }
request.HTTPBody = bodyData; request.HTTPBody = bodyData;
...@@ -1171,7 +1257,7 @@ ...@@ -1171,7 +1257,7 @@
if (error) { if (error) {
NSLog(@"❌ Network Error: %@", error); NSLog(@"❌ Network Error: %@", error);
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(nil, error); if (completion) completion(NO, nil, error);
}); });
return; return;
} }
...@@ -1186,7 +1272,13 @@ ...@@ -1186,7 +1272,13 @@
} }
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(jsonResponse, jsonParseError); if (completion) {
NSString *cacheKey = [NSString stringWithFormat:@"Issues_%@", [APIConfig projectId]];
NSDictionary *safeJson = [APIConfig dictionaryByReplacingNullsWithBlanks:jsonResponse];
[LocalStorage saveDictionary:safeJson forKey:cacheKey];
NSLog(@"💾 Saved to cache: %@", cacheKey);
completion(YES, jsonResponse, jsonParseError);
}
}); });
}]; }];
...@@ -1195,7 +1287,12 @@ ...@@ -1195,7 +1287,12 @@
+ (void)requestVoidIssue:(id)issueID + (void)requestVoidIssue:(id)issueID
remarks:(NSString *)remarks remarks:(NSString *)remarks
completion:(void (^)(NSDictionary *data, NSError *error))completion { completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion {
if ([APIConfig handleOfflineForAPI:@"VoidIssue" completion:completion]) {
return;
}
// ✅ URL // ✅ URL
NSURL *url = [APIConfig urlWithPath:@"/owner/issue/voidIssue"]; NSURL *url = [APIConfig urlWithPath:@"/owner/issue/voidIssue"];
...@@ -1227,7 +1324,7 @@ ...@@ -1227,7 +1324,7 @@
NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError]; NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError];
if (jsonError) { if (jsonError) {
NSLog(@"❌ JSON Serialization Error: %@", jsonError); NSLog(@"❌ JSON Serialization Error: %@", jsonError);
if (completion) completion(nil, jsonError); if (completion) completion(NO, nil, jsonError);
return; return;
} }
request.HTTPBody = bodyData; request.HTTPBody = bodyData;
...@@ -1244,7 +1341,7 @@ ...@@ -1244,7 +1341,7 @@
if (error) { if (error) {
NSLog(@"❌ Network Error: %@", error); NSLog(@"❌ Network Error: %@", error);
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(nil, error); if (completion) completion(NO, nil, error);
}); });
return; return;
} }
...@@ -1259,7 +1356,13 @@ ...@@ -1259,7 +1356,13 @@
} }
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(jsonResponse, jsonParseError); if (completion) {
NSString *cacheKey = [NSString stringWithFormat:@"VoidIssue_%@", [APIConfig projectId]];
NSDictionary *safeJson = [APIConfig dictionaryByReplacingNullsWithBlanks:jsonResponse];
[LocalStorage saveDictionary:safeJson forKey:cacheKey];
NSLog(@"💾 Saved to cache: %@", cacheKey);
completion(YES, jsonResponse, jsonParseError);
}
}); });
}]; }];
...@@ -1268,7 +1371,11 @@ ...@@ -1268,7 +1371,11 @@
+ (void)requestDeleteIssue:(NSString *)issueID + (void)requestDeleteIssue:(NSString *)issueID
remarks:(NSString *)remarks remarks:(NSString *)remarks
completion:(void (^)(NSDictionary *data, NSError *error))completion { completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion {
if ([APIConfig handleOfflineForAPI:@"DeleteIssue" completion:completion]) {
return;
}
// ✅ URL // ✅ URL
NSURL *url = [APIConfig urlWithPath:@"/owner/issue/deleteIssue"]; NSURL *url = [APIConfig urlWithPath:@"/owner/issue/deleteIssue"];
...@@ -1293,7 +1400,7 @@ ...@@ -1293,7 +1400,7 @@
NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError]; NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError];
if (jsonError) { if (jsonError) {
NSLog(@"❌ JSON Serialization Error: %@", jsonError); NSLog(@"❌ JSON Serialization Error: %@", jsonError);
if (completion) completion(nil, jsonError); if (completion) completion(NO, nil, jsonError);
return; return;
} }
request.HTTPBody = bodyData; request.HTTPBody = bodyData;
...@@ -1310,7 +1417,7 @@ ...@@ -1310,7 +1417,7 @@
if (error) { if (error) {
NSLog(@"❌ Network Error: %@", error); NSLog(@"❌ Network Error: %@", error);
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(nil, error); if (completion) completion(NO, nil, error);
}); });
return; return;
} }
...@@ -1325,14 +1432,24 @@ ...@@ -1325,14 +1432,24 @@
} }
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(jsonResponse, jsonParseError); if (completion) {
NSString *cacheKey = [NSString stringWithFormat:@"DeleteIssue_%@", [APIConfig projectId]];
NSDictionary *safeJson = [APIConfig dictionaryByReplacingNullsWithBlanks:jsonResponse];
[LocalStorage saveDictionary:safeJson forKey:cacheKey];
NSLog(@"💾 Saved to cache: %@", cacheKey);
completion(YES, jsonResponse, jsonParseError);
}
}); });
}]; }];
[task resume]; [task resume];
} }
+ (void)requestCommonArea:(void (^)(NSDictionary *data, NSError *error))completion { + (void)requestCommonArea:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion {
if ([APIConfig handleOfflineForAPI:@"CommonArea" completion:completion]) {
return;
}
// ✅ URL // ✅ URL
NSURL *url = [APIConfig urlWithPath:@"/owner/plan/getCommonArea"]; NSURL *url = [APIConfig urlWithPath:@"/owner/plan/getCommonArea"];
...@@ -1357,7 +1474,7 @@ ...@@ -1357,7 +1474,7 @@
NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError]; NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError];
if (jsonError) { if (jsonError) {
NSLog(@"❌ JSON Serialization Error: %@", jsonError); NSLog(@"❌ JSON Serialization Error: %@", jsonError);
if (completion) completion(nil, jsonError); if (completion) completion(NO, nil, jsonError);
return; return;
} }
request.HTTPBody = bodyData; request.HTTPBody = bodyData;
...@@ -1374,7 +1491,7 @@ ...@@ -1374,7 +1491,7 @@
if (error) { if (error) {
NSLog(@"❌ Network Error: %@", error); NSLog(@"❌ Network Error: %@", error);
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(nil, error); if (completion) completion(NO, nil, error);
}); });
return; return;
} }
...@@ -1389,14 +1506,24 @@ ...@@ -1389,14 +1506,24 @@
} }
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(jsonResponse, jsonParseError); if (completion) {
NSString *cacheKey = [NSString stringWithFormat:@"CommonArea_%@", [APIConfig projectId]];
NSDictionary *safeJson = [APIConfig dictionaryByReplacingNullsWithBlanks:jsonResponse];
[LocalStorage saveDictionary:safeJson forKey:cacheKey];
NSLog(@"💾 Saved to cache: %@", cacheKey);
completion(YES, jsonResponse, jsonParseError);
}
}); });
}]; }];
[task resume]; [task resume];
} }
+ (void)requestGeneralSettings:(void (^)(NSDictionary *data, NSError *error))completion { + (void)requestGeneralSettings:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion {
if ([APIConfig handleOfflineForAPI:@"GeneralSettings" completion:completion]) {
return;
}
// ✅ URL // ✅ URL
NSURL *url = [APIConfig urlWithPath:@"/owner/issue/getGeneralSetting"]; NSURL *url = [APIConfig urlWithPath:@"/owner/issue/getGeneralSetting"];
...@@ -1421,7 +1548,7 @@ ...@@ -1421,7 +1548,7 @@
NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError]; NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError];
if (jsonError) { if (jsonError) {
NSLog(@"❌ JSON Serialization Error: %@", jsonError); NSLog(@"❌ JSON Serialization Error: %@", jsonError);
if (completion) completion(nil, jsonError); if (completion) completion(NO, nil, jsonError);
return; return;
} }
request.HTTPBody = bodyData; request.HTTPBody = bodyData;
...@@ -1438,7 +1565,7 @@ ...@@ -1438,7 +1565,7 @@
if (error) { if (error) {
NSLog(@"❌ Network Error: %@", error); NSLog(@"❌ Network Error: %@", error);
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(nil, error); if (completion) completion(NO, nil, error);
}); });
return; return;
} }
...@@ -1453,14 +1580,24 @@ ...@@ -1453,14 +1580,24 @@
} }
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(jsonResponse, jsonParseError); if (completion) {
NSString *cacheKey = [NSString stringWithFormat:@"GeneralSettings_%@", [APIConfig projectId]];
NSDictionary *safeJson = [APIConfig dictionaryByReplacingNullsWithBlanks:jsonResponse];
[LocalStorage saveDictionary:safeJson forKey:cacheKey];
NSLog(@"💾 Saved to cache: %@", cacheKey);
completion(YES, jsonResponse, jsonParseError);
}
}); });
}]; }];
[task resume]; [task resume];
} }
+ (void)requestDefectMatrix:(void (^)(NSDictionary *data, NSError *error))completion { + (void)requestDefectMatrix:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion {
// if ([APIConfig handleOfflineForAPI:@"DefectMatrix" completion:completion]) {
// return;
// }
// ✅ URL // ✅ URL
NSURL *url = [APIConfig urlWithPath:@"/issue/getGeneralSettingByUnit"]; NSURL *url = [APIConfig urlWithPath:@"/issue/getGeneralSettingByUnit"];
...@@ -1486,7 +1623,7 @@ ...@@ -1486,7 +1623,7 @@
NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError]; NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError];
if (jsonError) { if (jsonError) {
NSLog(@"❌ JSON Serialization Error: %@", jsonError); NSLog(@"❌ JSON Serialization Error: %@", jsonError);
if (completion) completion(nil, jsonError); if (completion) completion(NO, nil, jsonError);
return; return;
} }
request.HTTPBody = bodyData; request.HTTPBody = bodyData;
...@@ -1503,7 +1640,7 @@ ...@@ -1503,7 +1640,7 @@
if (error) { if (error) {
NSLog(@"❌ Network Error: %@", error); NSLog(@"❌ Network Error: %@", error);
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(nil, error); if (completion) completion(NO, nil, error);
}); });
return; return;
} }
...@@ -1518,14 +1655,19 @@ ...@@ -1518,14 +1655,19 @@
} }
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(jsonResponse, jsonParseError); if (completion) {
completion(YES, jsonResponse, jsonParseError);
}
}); });
}]; }];
[task resume]; [task resume];
} }
+ (void)requestProjectList:(void (^)(NSDictionary *data, NSError *error))completion { + (void)requestProjectList:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion {
if ([APIConfig handleOfflineForAPI:@"ProjectList" completion:completion]) {
return;
}
// ✅ URL // ✅ URL
NSURL *url = [APIConfig urlWithPath:@"/owner/project/listProject"]; NSURL *url = [APIConfig urlWithPath:@"/owner/project/listProject"];
...@@ -1548,7 +1690,7 @@ ...@@ -1548,7 +1690,7 @@
NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError]; NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError];
if (jsonError) { if (jsonError) {
NSLog(@"❌ JSON Serialization Error: %@", jsonError); NSLog(@"❌ JSON Serialization Error: %@", jsonError);
if (completion) completion(nil, jsonError); if (completion) completion(NO, nil, jsonError);
return; return;
} }
request.HTTPBody = bodyData; request.HTTPBody = bodyData;
...@@ -1565,7 +1707,7 @@ ...@@ -1565,7 +1707,7 @@
if (error) { if (error) {
NSLog(@"❌ Network Error: %@", error); NSLog(@"❌ Network Error: %@", error);
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(nil, error); if (completion) completion(NO, nil, error);
}); });
return; return;
} }
...@@ -1580,7 +1722,13 @@ ...@@ -1580,7 +1722,13 @@
} }
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(jsonResponse, jsonParseError); if (completion) {
NSString *cacheKey = [NSString stringWithFormat:@"ProjectList_%@", [APIConfig projectId]];
NSDictionary *safeJson = [APIConfig dictionaryByReplacingNullsWithBlanks:jsonResponse];
[LocalStorage saveDictionary:safeJson forKey:cacheKey];
NSLog(@"💾 Saved to cache: %@", cacheKey);
completion(YES, jsonResponse, jsonParseError);
}
}); });
}]; }];
...@@ -1588,7 +1736,11 @@ ...@@ -1588,7 +1736,11 @@
} }
+ (void)requestArrayImage:(NSArray *)imageArray + (void)requestArrayImage:(NSArray *)imageArray
completion:(void (^)(NSDictionary *data, NSError *error))completion { completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion {
if ([APIConfig handleOfflineForAPI:@"ArrayImage" completion:completion]) {
return;
}
// ✅ URL // ✅ URL
NSURL *url = [APIConfig urlWithPath:@"/owner/offline/syncImageArray"]; NSURL *url = [APIConfig urlWithPath:@"/owner/offline/syncImageArray"];
...@@ -1610,7 +1762,7 @@ ...@@ -1610,7 +1762,7 @@
NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError]; NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError];
if (jsonError) { if (jsonError) {
NSLog(@"❌ JSON Serialization Error: %@", jsonError); NSLog(@"❌ JSON Serialization Error: %@", jsonError);
if (completion) completion(nil, jsonError); if (completion) completion(NO, nil, jsonError);
return; return;
} }
request.HTTPBody = bodyData; request.HTTPBody = bodyData;
...@@ -1627,7 +1779,7 @@ ...@@ -1627,7 +1779,7 @@
if (error) { if (error) {
NSLog(@"❌ Network Error: %@", error); NSLog(@"❌ Network Error: %@", error);
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(nil, error); if (completion) completion(NO, nil, error);
}); });
return; return;
} }
...@@ -1642,86 +1794,113 @@ ...@@ -1642,86 +1794,113 @@
} }
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(jsonResponse, jsonParseError); if (completion) {
NSString *cacheKey = [NSString stringWithFormat:@"ArrayImage_%@", [APIConfig projectId]];
NSDictionary *safeJson = [APIConfig dictionaryByReplacingNullsWithBlanks:jsonResponse];
[LocalStorage saveDictionary:safeJson forKey:cacheKey];
NSLog(@"💾 Saved to cache: %@", cacheKey);
completion(YES, jsonResponse, jsonParseError);
}
}); });
}]; }];
[task resume]; [task resume];
} }
// + (void)downloadAndCacheImage:(NSURL *)url + (void)downloadAndCacheImage:(NSURL *)url
// completion:(void (^)(BOOL success))completion { completion:(void (^)(BOOL success, NSString *filePath))completion {
//
// if (!url) { if (!url) {
// if (completion) completion(NO); if (completion) completion(NO, nil);
// return; return;
// } }
//
// // 1️⃣ Build cache path // 1️⃣ Get cache directory
// NSString *fileName = url.lastPathComponent; NSString *cacheDir = [self offlineImageCacheDirectory];
// // NSString *cacheDir = [self offlineImageCacheDirectory]; NSFileManager *fm = [NSFileManager defaultManager];
// NSString *filePath = [cacheDir stringByAppendingPathComponent:fileName];
// if (![fm fileExistsAtPath:cacheDir]) {
// // 2️⃣ Skip if already cached NSError *dirError = nil;
// if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { [fm createDirectoryAtPath:cacheDir
// NSLog(@"🟢 Cached file exists: %@", fileName); withIntermediateDirectories:YES
// if (completion) completion(YES); attributes:nil
// return; error:&dirError];
// } if (dirError) {
// NSLog(@"❌ Failed to create cache directory: %@", dirError);
// // 3️⃣ Build request with timeout if (completion) completion(NO, nil);
// NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; return;
// request.HTTPMethod = @"GET"; }
// request.timeoutInterval = 20; // ⏱️ per-file timeout }
//
// NSURLSessionConfiguration *config = // 2️⃣ Generate unique file name (avoid collisions)
// [NSURLSessionConfiguration defaultSessionConfiguration]; NSString *fileName = [NSString stringWithFormat:@"%@_%@", url.lastPathComponent, [self md5Hash:url.absoluteString]];
// config.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData; NSString *filePath = [cacheDir stringByAppendingPathComponent:fileName];
//
// NSURLSession *session = [NSURLSession sessionWithConfiguration:config]; // 3️⃣ Skip if already cached
// if ([fm fileExistsAtPath:filePath]) {
// // 4️⃣ Download task NSLog(@"🟢 Cached file exists: %@", fileName);
// NSURLSessionDownloadTask *task = if (completion) completion(YES, filePath);
// [session downloadTaskWithRequest:request return;
// completionHandler:^(NSURL *location, }
// NSURLResponse *response,
// NSError *error) { // 4️⃣ Build request
// NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// if (error || !location) { request.HTTPMethod = @"GET";
// NSLog(@"❌ Download failed: %@ | %@", url, error); request.timeoutInterval = 20; // per-file timeout
// dispatch_async(dispatch_get_main_queue(), ^{
// if (completion) completion(NO); NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
// }); config.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
// return;
// } NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
//
// // 5️⃣ Move file to cache // 5️⃣ Download task
// NSError *moveError; NSURLSessionDownloadTask *task =
// [[NSFileManager defaultManager] [session downloadTaskWithRequest:request
// moveItemAtURL:location completionHandler:^(NSURL *location,
// toURL:[NSURL fileURLWithPath:filePath] NSURLResponse *response,
// error:&moveError]; NSError *error) {
//
// if (moveError) { if (error || !location) {
// NSLog(@"❌ Save failed: %@", moveError); NSLog(@"❌ Download failed: %@ | %@", url, error);
// dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
// if (completion) completion(NO); if (completion) completion(NO, nil);
// }); });
// return; return;
// } }
//
// NSLog(@"✅ Cached: %@", fileName); // 6️⃣ Move file to cache (on background queue)
// dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
// dispatch_async(dispatch_get_main_queue(), ^{ NSError *moveError = nil;
// if (completion) completion(YES); [fm moveItemAtURL:location
// }); toURL:[NSURL fileURLWithPath:filePath]
// }]; error:&moveError];
//
// [task resume]; if (moveError) {
// } NSLog(@"❌ Save failed: %@", moveError);
dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(NO, nil);
});
return;
}
NSLog(@"✅ Cached: %@", fileName);
// 7️⃣ Completion on main queue
dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(YES, filePath);
});
});
}];
[task resume];
}
+ (void)requestCompanyCode:(NSString *)companyCode + (void)requestCompanyCode:(NSString *)companyCode
completion:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion; { completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion; {
if ([APIConfig handleOfflineForAPI:@"CompanyCode" completion:completion]) {
return;
}
NSString *urlString = @"https://kitadev.commudesk.com/api/companycode/get"; NSString *urlString = @"https://kitadev.commudesk.com/api/companycode/get";
NSURL *url = [NSURL URLWithString:urlString]; NSURL *url = [NSURL URLWithString:urlString];
...@@ -1745,7 +1924,7 @@ ...@@ -1745,7 +1924,7 @@
NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError]; NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError];
if (jsonError) { if (jsonError) {
NSLog(@"❌ JSON Serialization Error: %@", jsonError); NSLog(@"❌ JSON Serialization Error: %@", jsonError);
if (completion) completion(nil, jsonError); if (completion) completion(NO, nil, jsonError);
return; return;
} }
request.HTTPBody = bodyData; request.HTTPBody = bodyData;
...@@ -1762,7 +1941,7 @@ ...@@ -1762,7 +1941,7 @@
if (error) { if (error) {
NSLog(@"❌ Network Error: %@", error); NSLog(@"❌ Network Error: %@", error);
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(nil, error); if (completion) completion(NO, nil, error);
}); });
return; return;
} }
...@@ -1777,7 +1956,13 @@ ...@@ -1777,7 +1956,13 @@
} }
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(jsonResponse, jsonParseError); if (completion) {
NSString *cacheKey = [NSString stringWithFormat:@"CompanyCode_%@", [APIConfig projectId]];
NSDictionary *safeJson = [APIConfig dictionaryByReplacingNullsWithBlanks:jsonResponse];
[LocalStorage saveDictionary:safeJson forKey:cacheKey];
NSLog(@"💾 Saved to cache: %@", cacheKey);
completion(YES, jsonResponse, jsonParseError);
}
}); });
}]; }];
[task resume]; [task resume];
...@@ -1801,4 +1986,24 @@ ...@@ -1801,4 +1986,24 @@
return data; return data;
} }
+ (NSString *)md5Hash:(NSString *)input {
if (!input) return nil;
const char *cStr = [input UTF8String];
unsigned char digest[CC_MD5_DIGEST_LENGTH];
CC_MD5(cStr, (CC_LONG)strlen(cStr), digest);
NSMutableString *output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++)
[output appendFormat:@"%02x", digest[i]];
return output;
}
+ (NSString *)offlineImageCacheDirectory {
NSString *docsDir = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
NSString *cacheDir = [docsDir stringByAppendingPathComponent:@"OfflineUnitImages"];
return cacheDir;
}
@end @end
...@@ -33,6 +33,13 @@ NS_ASSUME_NONNULL_BEGIN ...@@ -33,6 +33,13 @@ NS_ASSUME_NONNULL_BEGIN
/// JSON string form (what your APIs expect) /// JSON string form (what your APIs expect)
+ (NSString *)deviceInfoJSONString; + (NSString *)deviceInfoJSONString;
//Offline
+ (BOOL)handleOfflineForAPI:(NSString *)apiName
completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion;
//Null
+ (NSDictionary *)dictionaryByReplacingNullsWithBlanks:(NSDictionary *)dict;
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END
......
// APIConfig.mm // APIConfig.mm
#import "APIConfig.h" #import "APIConfig.h"
#import "LocalStorage.h"
#import "NetworkManager.h"
static NSString *kBaseURL = @"https://kitadev.commudesk.com/api"; static NSString *kBaseURL = @"https://kitadev.commudesk.com/api";
static NSString *kDrawingPlanId = @""; static NSString *kDrawingPlanId = @"";
...@@ -89,4 +91,67 @@ static NSString *kAuthToken = @"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI ...@@ -89,4 +91,67 @@ static NSString *kAuthToken = @"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
} }
#pragma mark - Offline Handling
+ (BOOL)handleOfflineForAPI:(NSString *)apiName
completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion {
NSString *cacheKey =
[NSString stringWithFormat:@"%@_%@", apiName, [self projectId]];
if (![NetworkManager.sharedManager isConnected]) {
NSDictionary *cached = [LocalStorage loadDictionaryForKey:cacheKey];
if (cached) {
NSLog(@"🟡 Loaded from cache: %@", cacheKey);
if (completion) completion(YES, cached, nil);
} else {
NSError *offlineErr =
[NSError errorWithDomain:@"OfflineError"
code:-1
userInfo:@{
NSLocalizedDescriptionKey:
@"No network and no cached data"
}];
if (completion) completion(NO, nil, offlineErr);
}
return YES; // offline handled
}
return NO; // proceed with online request
}
#pragma mark - Null Handling
+ (NSDictionary *)dictionaryByReplacingNullsWithBlanks:(NSDictionary *)dict {
NSMutableDictionary *result = [NSMutableDictionary dictionary];
for (NSString *key in dict) {
id value = dict[key];
if (value == [NSNull null]) {
result[key] = @""; // or NSNull-safe replacement
} else if ([value isKindOfClass:[NSDictionary class]]) {
result[key] = [self dictionaryByReplacingNullsWithBlanks:value];
} else if ([value isKindOfClass:[NSArray class]]) {
result[key] = [self arrayByReplacingNullsWithBlanks:value];
} else {
result[key] = value;
}
}
return result;
}
+ (NSArray *)arrayByReplacingNullsWithBlanks:(NSArray *)array {
NSMutableArray *result = [NSMutableArray array];
for (id obj in array) {
if (obj == [NSNull null]) {
[result addObject:@""]; // or other safe value
} else if ([obj isKindOfClass:[NSDictionary class]]) {
[result addObject:[self dictionaryByReplacingNullsWithBlanks:obj]];
} else if ([obj isKindOfClass:[NSArray class]]) {
[result addObject:[self arrayByReplacingNullsWithBlanks:obj]];
} else {
[result addObject:obj];
}
}
return result;
}
@end @end
...@@ -277,8 +277,7 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE"; ...@@ -277,8 +277,7 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
[APIClient submitAddInfo:item[@"issue_id"] [APIClient submitAddInfo:item[@"issue_id"]
remarks:item[@"remarks"] remarks:item[@"remarks"]
completion:^(NSDictionary *res, NSError *err) { completion:^(BOOL success, NSDictionary *res, NSError *err) {
BOOL success = [weakSelf isAPISuccess:res error:err];
if (!success) NSLog(@"⚠️ ADD_INFO failed: %@", err ?: res); if (!success) NSLog(@"⚠️ ADD_INFO failed: %@", err ?: res);
finalize(success); finalize(success);
}]; }];
...@@ -290,8 +289,7 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE"; ...@@ -290,8 +289,7 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
images:images images:images
mediaURL:mediaURL mediaURL:mediaURL
mediaType:item[@"mediaType"] mediaType:item[@"mediaType"]
completion:^(NSDictionary *res, NSError *err) { completion:^(BOOL success, NSDictionary *res, NSError *err) {
BOOL success = [weakSelf isAPISuccess:res error:err];
if (!success) NSLog(@"⚠️ ADD_INFO upload failed: %@", err ?: res); if (!success) NSLog(@"⚠️ ADD_INFO upload failed: %@", err ?: res);
finalize(success); finalize(success);
}]; }];
...@@ -303,11 +301,9 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE"; ...@@ -303,11 +301,9 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
[APIClient submitAddIssue:item[@"payload"] [APIClient submitAddIssue:item[@"payload"]
images:images images:images
completion:^(NSDictionary *res, NSError *err) { completion:^(BOOL success, NSDictionary *res, NSError *err) {
[weakSelf logAddIssueDebugWithItem:item images:images res:res err:err]; [weakSelf logAddIssueDebugWithItem:item images:images res:res err:err];
BOOL success = [weakSelf isAPISuccess:res error:err];
if (!success) NSLog(@"⚠️ ADD_ISSUE failed: %@", err ?: res); if (!success) NSLog(@"⚠️ ADD_ISSUE failed: %@", err ?: res);
finalize(success); finalize(success);
}]; }];
...@@ -318,8 +314,7 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE"; ...@@ -318,8 +314,7 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
[APIClient submitUpdateIssue:item[@"payload"] [APIClient submitUpdateIssue:item[@"payload"]
images:images images:images
completion:^(NSDictionary *res, NSError *err) { completion:^(BOOL success, NSDictionary *res, NSError *err) {
BOOL success = [weakSelf isAPISuccess:res error:err];
if (!success) NSLog(@"⚠️ UPDATE_ISSUE failed: %@", err ?: res); if (!success) NSLog(@"⚠️ UPDATE_ISSUE failed: %@", err ?: res);
finalize(success); finalize(success);
}]; }];
...@@ -559,22 +554,20 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE"; ...@@ -559,22 +554,20 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
// Task 1 — requestIssues // Task 1 — requestIssues
[tasks addObject:^(void (^next)(void)) { [tasks addObject:^(void (^next)(void)) {
[self safeSequentialTask:@"task1" block:^(void (^safeNext)(void), void (^logSuccess)(BOOL)) { [self safeSequentialTask:@"task1" block:^(void (^safeNext)(void), void (^logSuccess)(BOOL)) {
NSLog(@"🚀 Starting Task 1");
NSString *key = [NSString stringWithFormat:@"ISSUES_%@", drawingPlanId]; [APIClient requestIssues:^(BOOL success, NSDictionary *res, NSError *err) {
NSLog(@"🚀 Starting Task 1 — requestIssues (key: %@)", key);
[APIClient requestIssues:^(NSDictionary *res, NSError *err) {
NSString *logText = [NSString stringWithFormat: NSString *logText = [NSString stringWithFormat:
@"\n🚀 Task 1 — requestIssues\nTime: %@\nError: %@\nResponse: %@\n", @"\n🚀 Task 1 — requestIssues\nTime: %@\nError: %@\nResponse: %@\n",
[NSDate date], err, res]; [NSDate date], err, res];
[self appendOfflineLog:logText]; [self appendOfflineLog:logText];
if (err) { if (!success) {
NSLog(@"⚠️ Task 1 failed: %@", err); NSLog(@"⚠️ Task 1 failed: %@", err);
logSuccess(NO); logSuccess(NO);
} else { } else {
if (res) [LocalStorage saveDictionary:res[@"Data"] forKey:key]; if (res)
NSLog(@"✅ Task 1 succeeded, saved to key: %@", key); NSLog(@"✅ Task 1 succeeded");
logSuccess(YES); logSuccess(YES);
} }
safeNext(); safeNext();
...@@ -586,21 +579,19 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE"; ...@@ -586,21 +579,19 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
// Task 2 — requestDashboardInfo // Task 2 — requestDashboardInfo
[tasks addObject:^(void (^next)(void)) { [tasks addObject:^(void (^next)(void)) {
[self safeSequentialTask:@"task2" block:^(void (^safeNext)(void), void (^logSuccess)(BOOL)) { [self safeSequentialTask:@"task2" block:^(void (^safeNext)(void), void (^logSuccess)(BOOL)) {
NSLog(@"🚀 Starting Task 2");
NSString *key = [NSString stringWithFormat:@"DASHBOARD_INFO_%@", drawingPlanId]; [APIClient requestDashboardInfo:^(BOOL success, NSDictionary *res, NSError *err) {
NSLog(@"🚀 Starting Task 2 — requestDashboardInfo (key: %@)", key);
[APIClient requestDashboardInfo:^(NSDictionary *res, NSError *err) {
NSString *logText = [NSString stringWithFormat: NSString *logText = [NSString stringWithFormat:
@"\n🚀 Task 2 — requestDashboardInfo\nTime: %@\nError: %@\nResponse: %@\n", @"\n🚀 Task 2 — requestDashboardInfo\nTime: %@\nError: %@\nResponse: %@\n",
[NSDate date], err, res]; [NSDate date], err, res];
[self appendOfflineLog:logText]; [self appendOfflineLog:logText];
if (err) { if (!success) {
NSLog(@"⚠️ Task 2 failed: %@", err); NSLog(@"⚠️ Task 2 failed: %@", err);
logSuccess(NO); logSuccess(NO);
} else { } else {
if (res) [LocalStorage saveDictionary:res[@"Data"] forKey:key]; if (res)
NSLog(@"✅ Task 2 succeeded, saved to key: %@", key); NSLog(@"✅ Task 2 succeeded");
logSuccess(YES); logSuccess(YES);
} }
safeNext(); safeNext();
...@@ -612,11 +603,9 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE"; ...@@ -612,11 +603,9 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
// Task 3 — requestGetUnitPlan (already has logs) // Task 3 — requestGetUnitPlan (already has logs)
[tasks addObject:^(void (^next)(void)) { [tasks addObject:^(void (^next)(void)) {
[self safeSequentialTask:@"task3" block:^(void (^safeNext)(void), void (^logSuccess)(BOOL)) { [self safeSequentialTask:@"task3" block:^(void (^safeNext)(void), void (^logSuccess)(BOOL)) {
NSLog(@"🚀 Starting Task 3");
NSString *key = [NSString stringWithFormat:@"PLAN_UNIT_%@", drawingPlanId]; [APIClient requestGetUnitPlan:^(BOOL success, NSDictionary *res, NSError *err) {
NSLog(@"🚀 Starting Task 3 — requestGetUnitPlan (key: %@)", key);
[APIClient requestGetUnitPlan:^(NSDictionary *res, NSError *err) {
NSString *logText = [NSString stringWithFormat: NSString *logText = [NSString stringWithFormat:
@"\n🚀 Task 3 — requestGetUnitPlan\nTime: %@\nError: %@\nResponse: %@\n", @"\n🚀 Task 3 — requestGetUnitPlan\nTime: %@\nError: %@\nResponse: %@\n",
[NSDate date], err, res]; [NSDate date], err, res];
...@@ -636,8 +625,7 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE"; ...@@ -636,8 +625,7 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
if ([firstItem isKindOfClass:[NSDictionary class]]) objData = firstItem; if ([firstItem isKindOfClass:[NSDictionary class]]) objData = firstItem;
} }
[LocalStorage saveDictionary:objData forKey:key]; NSLog(@"✅ Task 3 succeeded");
NSLog(@"✅ Task 3 succeeded, saved to key: %@", key);
logSuccess(YES); logSuccess(YES);
safeNext(); safeNext();
}]; }];
...@@ -648,22 +636,20 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE"; ...@@ -648,22 +636,20 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
// Task 4 — requestGeneralSettings // Task 4 — requestGeneralSettings
[tasks addObject:^(void (^next)(void)) { [tasks addObject:^(void (^next)(void)) {
[self safeSequentialTask:@"task4" block:^(void (^safeNext)(void), void (^logSuccess)(BOOL)) { [self safeSequentialTask:@"task4" block:^(void (^safeNext)(void), void (^logSuccess)(BOOL)) {
NSLog(@"🚀 Starting Task 4");
NSString *key = [NSString stringWithFormat:@"SETTINGS_%@", projectId]; [APIClient requestGeneralSettings:^(BOOL success, NSDictionary *res, NSError *err) {
NSLog(@"🚀 Starting Task 4 — requestGeneralSettings (key: %@)", key);
[APIClient requestGeneralSettings:^(NSDictionary *res, NSError *err) {
NSString *logText = [NSString stringWithFormat: NSString *logText = [NSString stringWithFormat:
@"\n🚀 Task 4 — requestGeneralSettings\nTime: %@\nError: %@\nResponse: %@\n", @"\n🚀 Task 4 — requestGeneralSettings\nTime: %@\nError: %@\nResponse: %@\n",
[NSDate date], err, res]; [NSDate date], err, res];
[self appendOfflineLog:logText]; [self appendOfflineLog:logText];
if (err) { if (!success) {
NSLog(@"⚠️ Task 4 failed: %@", err); NSLog(@"⚠️ Task 4 failed: %@", err);
logSuccess(NO); logSuccess(NO);
} else { } else {
if (res) [LocalStorage saveDictionary:res[@"Data"] forKey:key]; if (res)
NSLog(@"✅ Task 4 succeeded, saved to key: %@", key); NSLog(@"✅ Task 4 succeeded");
logSuccess(YES); logSuccess(YES);
} }
safeNext(); safeNext();
...@@ -675,22 +661,19 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE"; ...@@ -675,22 +661,19 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
// Task 5 — requestCommonArea // Task 5 — requestCommonArea
[tasks addObject:^(void (^next)(void)) { [tasks addObject:^(void (^next)(void)) {
[self safeSequentialTask:@"task5" block:^(void (^safeNext)(void), void (^logSuccess)(BOOL)) { [self safeSequentialTask:@"task5" block:^(void (^safeNext)(void), void (^logSuccess)(BOOL)) {
NSLog(@"🚀 Starting Task 5");
NSString *key = [NSString stringWithFormat:@"COMMONAREA_%@", projectId]; [APIClient requestCommonArea:^(BOOL success, NSDictionary *res, NSError *err) {
NSLog(@"🚀 Starting Task 5 — requestCommonArea (key: %@)", key);
[APIClient requestCommonArea:^(NSDictionary *res, NSError *err) {
NSString *logText = [NSString stringWithFormat: NSString *logText = [NSString stringWithFormat:
@"\n🚀 Task 5 — requestCommonArea\nTime: %@\nError: %@\nResponse: %@\n", @"\n🚀 Task 5 — requestCommonArea\nTime: %@\nError: %@\nResponse: %@\n",
[NSDate date], err, res]; [NSDate date], err, res];
[self appendOfflineLog:logText]; [self appendOfflineLog:logText];
if (err) { if (!success) {
NSLog(@"⚠️ Task 5 failed: %@", err); NSLog(@"⚠️ Task 5 failed: %@", err);
logSuccess(NO); logSuccess(NO);
} else { } else {
if (res) [LocalStorage saveDictionary:res[@"Data"] forKey:key]; if (res)
NSLog(@"✅ Task 5 succeeded, saved to key: %@", key); NSLog(@"✅ Task 5 succeeded");
logSuccess(YES); logSuccess(YES);
} }
safeNext(); safeNext();
...@@ -705,7 +688,7 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE"; ...@@ -705,7 +688,7 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
NSLog(@"🚀 Starting Task 6 — requestDefectMatrix"); NSLog(@"🚀 Starting Task 6 — requestDefectMatrix");
[APIClient requestDefectMatrix:^(NSDictionary *res, NSError *err) { [APIClient requestDefectMatrix:^(BOOL success, NSDictionary *res, NSError *err) {
NSString *logText = [NSString stringWithFormat: NSString *logText = [NSString stringWithFormat:
@"\n🚀 Task 6 — requestDefectMatrix\nTime: %@\nError: %@\nResponse: %@\n", @"\n🚀 Task 6 — requestDefectMatrix\nTime: %@\nError: %@\nResponse: %@\n",
[NSDate date], err, res]; [NSDate date], err, res];
...@@ -768,7 +751,102 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE"; ...@@ -768,7 +751,102 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
} next:next]; } next:next];
}]; }];
// Task 7 — Image downloads // Task 7 — requestAccessItem
[tasks addObject:^(void (^next)(void)) {
[self safeSequentialTask:@"task7" block:^(void (^safeNext)(void), void (^logSuccess)(BOOL)) {
NSLog(@"🚀 Starting Task 7");
[APIClient requestAccessItem:^(BOOL success, NSDictionary *res, NSError *err) {
NSString *logText = [NSString stringWithFormat:
@"\n🚀 Task 7 — requestAccessItem\nTime: %@\nError: %@\nResponse: %@\n",
[NSDate date], err, res];
[self appendOfflineLog:logText];
if (!success) {
NSLog(@"⚠️ Task 7 failed: %@", err);
logSuccess(NO);
} else {
if (res)
NSLog(@"✅ Task 7 succeeded");
logSuccess(YES);
}
safeNext();
}];
} next:next];
}];
// Task 8 — requestGeneralInfo
[tasks addObject:^(void (^next)(void)) {
[self safeSequentialTask:@"task8" block:^(void (^safeNext)(void), void (^logSuccess)(BOOL)) {
NSLog(@"🚀 Starting Task 4");
[APIClient requestGeneralInfo:^(BOOL success, NSDictionary *res, NSError *err) {
NSString *logText = [NSString stringWithFormat:
@"\n🚀 Task 8 — requestGeneralInfo\nTime: %@\nError: %@\nResponse: %@\n",
[NSDate date], err, res];
[self appendOfflineLog:logText];
if (!success) {
NSLog(@"⚠️ Task 8 failed: %@", err);
logSuccess(NO);
} else {
if (res)
NSLog(@"✅ Task 8 succeeded");
logSuccess(YES);
}
safeNext();
}];
} next:next];
}];
// Task 9 — requestAnnouncement
[tasks addObject:^(void (^next)(void)) {
[self safeSequentialTask:@"task9" block:^(void (^safeNext)(void), void (^logSuccess)(BOOL)) {
NSLog(@"🚀 Starting Task 9");
[APIClient requestAnnouncement:^(BOOL success, NSDictionary *res, NSError *err) {
NSString *logText = [NSString stringWithFormat:
@"\n🚀 Task 9 — requestAnnouncement\nTime: %@\nError: %@\nResponse: %@\n",
[NSDate date], err, res];
[self appendOfflineLog:logText];
if (!success) {
NSLog(@"⚠️ Task 9 failed: %@", err);
logSuccess(NO);
} else {
if (res)
NSLog(@"✅ Task 9 succeeded");
logSuccess(YES);
}
safeNext();
}];
} next:next];
}];
// Task 10 — requestAppointment
[tasks addObject:^(void (^next)(void)) {
[self safeSequentialTask:@"task10" block:^(void (^safeNext)(void), void (^logSuccess)(BOOL)) {
NSLog(@"🚀 Starting Task 10");
[APIClient requestAppointment:^(BOOL success, NSDictionary *res, NSError *err) {
NSString *logText = [NSString stringWithFormat:
@"\n🚀 Task 10 — requestAppointment\nTime: %@\nError: %@\nResponse: %@\n",
[NSDate date], err, res];
[self appendOfflineLog:logText];
if (!success) {
NSLog(@"⚠️ Task 10 failed: %@", err);
logSuccess(NO);
} else {
if (res)
NSLog(@"✅ Task 10 succeeded");
logSuccess(YES);
}
safeNext();
}];
} next:next];
}];
// Task 11 — Image downloads
if (selectedUnits.count > 0) { if (selectedUnits.count > 0) {
[self fetchOfflineImageListForSelectedUnits:selectedUnits completion:^(NSArray<NSURL *> *urls, NSError *error) { [self fetchOfflineImageListForSelectedUnits:selectedUnits completion:^(NSArray<NSURL *> *urls, NSError *error) {
if (error || urls.count == 0) { if (error || urls.count == 0) {
...@@ -777,30 +855,30 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE"; ...@@ -777,30 +855,30 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
// Flatten all images into tasks // Flatten all images into tasks
for (NSInteger i = 0; i < urls.count; i++) { for (NSInteger i = 0; i < urls.count; i++) {
NSURL *url = urls[i]; NSURL *url = urls[i];
[tasks addObject:^(void (^next)(void)) { [tasks addObject:^(void (^next)(void)) {
NSString *urlKey = [NSString stringWithFormat:@"image_%ld", (long)i]; NSString *urlKey = [NSString stringWithFormat:@"image_%ld", (long)i];
__block BOOL finished = NO;
// Timeout guard per image [APIClient downloadAndCacheImage:url completion:^(BOOL success, NSString *filePath) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (!finished) { // Save status to local storage (include file path for debugging)
NSLog(@"⚠️ Image download timeout: %@", url);
NSMutableDictionary *status = NSMutableDictionary *status =
[[LocalStorage loadDictionaryForKey:@"OFFLINE_DOWNLOAD_STATUS"] mutableCopy] ?: [NSMutableDictionary dictionary]; [[LocalStorage loadDictionaryForKey:@"OFFLINE_DOWNLOAD_STATUS"] mutableCopy] ?: [NSMutableDictionary dictionary];
status[urlKey] = @"FAILED";
status[urlKey] = @{
@"status": success ? @"SUCCESS" : @"FAILED",
@"filePath": filePath ?: @""
};
[LocalStorage saveDictionary:status forKey:@"OFFLINE_DOWNLOAD_STATUS"]; [LocalStorage saveDictionary:status forKey:@"OFFLINE_DOWNLOAD_STATUS"];
finished = YES;
if (next) next(); if (!success) {
NSLog(@"⚠️ Image download failed for URL: %@", url);
} else {
NSLog(@"✅ Image downloaded: %@", filePath);
} }
});
[APIClient downloadAndCacheImage:url completion:^(BOOL success) { // Continue with next task
if (finished) return;
finished = YES;
NSMutableDictionary *status =
[[LocalStorage loadDictionaryForKey:@"OFFLINE_DOWNLOAD_STATUS"] mutableCopy] ?: [NSMutableDictionary dictionary];
status[urlKey] = success ? @"SUCCESS" : @"FAILED";
[LocalStorage saveDictionary:status forKey:@"OFFLINE_DOWNLOAD_STATUS"];
if (next) next(); if (next) next();
}]; }];
}]; }];
...@@ -861,7 +939,6 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE"; ...@@ -861,7 +939,6 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
task(runNextTask); task(runNextTask);
}); });
}; };
runNextTask(); runNextTask();
} }
...@@ -895,9 +972,9 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE"; ...@@ -895,9 +972,9 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
return; return;
} }
[APIClient requestArrayImage:payload completion:^(NSDictionary *res, NSError *err) { [APIClient requestArrayImage:payload completion:^(BOOL success, NSDictionary *res, NSError *error) {
if (err) { if (!success) {
completion(nil, err); completion(nil, error);
return; return;
} }
......
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