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

code update

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