Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
W
weihan-plugin
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Wei Han
weihan-plugin
Commits
82cef0dc
Commit
82cef0dc
authored
Jan 09, 2026
by
Wei Han
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
code update
parent
4a7abd98
Show whitespace changes
Inline
Side-by-side
Showing
24 changed files
with
1304 additions
and
676 deletions
+1304
-676
project.pbxproj
...ginFramework/QmsPluginFramework.xcodeproj/project.pbxproj
+4
-0
AccessAppointmentViewController.mm
...inFramework/AccessItem/AccessAppointmentViewController.mm
+1
-1
AppointmentDetailsViewController.mm
...Framework/Appointment/AppointmentDetailsViewController.mm
+2
-2
AppointmentViewController.mm
...sPluginFramework/Appointment/AppointmentViewController.mm
+33
-19
AppointmentViewMoreViewController.mm
...ramework/Appointment/AppointmentViewMoreViewController.mm
+1
-1
DashboardViewController.h
...rk/QmsPluginFramework/Dashboard/DashboardViewController.h
+1
-0
DashboardViewController.mm
...k/QmsPluginFramework/Dashboard/DashboardViewController.mm
+610
-374
GeneralInfoViewController.mm
...QmsPluginFramework/Dashboard/GeneralInfoViewController.mm
+1
-1
ProjectViewController.mm
...ork/QmsPluginFramework/Dashboard/ProjectViewController.mm
+31
-15
UnitViewController.h
...amework/QmsPluginFramework/Dashboard/UnitViewController.h
+1
-0
UnitViewController.mm
...mework/QmsPluginFramework/Dashboard/UnitViewController.mm
+2
-1
AddInfoViewController.mm
...amework/QmsPluginFramework/Issue/AddInfoViewController.mm
+2
-2
DetailsViewController.mm
...msPluginFramework/Issue/AddIssue/DetailsViewController.mm
+2
-2
PlanViewController.mm
...k/QmsPluginFramework/Issue/AddIssue/PlanViewController.mm
+2
-2
HistoryViewController.mm
...amework/QmsPluginFramework/Issue/HistoryViewController.mm
+1
-1
IssuesViewController.mm
...ramework/QmsPluginFramework/Issue/IssuesViewController.mm
+1
-1
WithdrawIssuesViewController.mm
.../QmsPluginFramework/Issue/WithdrawIssuesViewController.mm
+2
-2
WithdrawSingleIssueViewController.mm
...luginFramework/Issue/WithdrawSingleIssueViewController.mm
+2
-2
SyncProjectViewController.mm
...work/QmsPluginFramework/Sync/SyncProjectViewController.mm
+2
-1
APIClient.h
...ginFramework/QmsPluginFramework/Utilities/API/APIClient.h
+22
-22
APIClient.mm
...inFramework/QmsPluginFramework/Utilities/API/APIClient.mm
+366
-161
APIConfig.h
...ginFramework/QmsPluginFramework/Utilities/API/APIConfig.h
+7
-0
APIConfig.mm
...inFramework/QmsPluginFramework/Utilities/API/APIConfig.mm
+65
-0
OfflineSyncManager.mm
...uginFramework/Utilities/OfflineSync/OfflineSyncManager.mm
+143
-66
No files found.
framework/QmsPluginFramework/QmsPluginFramework.xcodeproj/project.pbxproj
View file @
82cef0dc
...
...
@@ -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
;
...
...
framework/QmsPluginFramework/QmsPluginFramework/AccessItem/AccessAppointmentViewController.mm
View file @
82cef0dc
...
...
@@ -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(), ^{
...
...
framework/QmsPluginFramework/QmsPluginFramework/Appointment/AppointmentDetailsViewController.mm
View file @
82cef0dc
...
...
@@ -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"];
...
...
framework/QmsPluginFramework/QmsPluginFramework/Appointment/AppointmentViewController.mm
View file @
82cef0dc
...
...
@@ -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 c
olorWithWhite:0.95 alpha:1.0
];
self.termsContainer.backgroundColor = [UIColor c
learColor
];
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
:
14
px
;
font-family
:
-apple-system
;
line-height
:
1.5
;
padding
:
12px
;
background-color
:
#F9F9F9
;
color
:
#000000
;
}
"
"
body
{
font-size
:
16
px
;
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);
...
...
framework/QmsPluginFramework/QmsPluginFramework/Appointment/AppointmentViewMoreViewController.mm
View file @
82cef0dc
...
...
@@ -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];
...
...
framework/QmsPluginFramework/QmsPluginFramework/Dashboard/DashboardViewController.h
View file @
82cef0dc
...
...
@@ -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
...
...
framework/QmsPluginFramework/QmsPluginFramework/Dashboard/DashboardViewController.mm
View file @
82cef0dc
...
...
@@ -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)setup
InspectionChecklistBanner {
- (
UIView *)build
InspectionChecklistBanner {
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] init
WithFrame:CGRectMake(CGRectGetMaxX(statusDot.frame) + 6, 8, width - 160, 22)
];
statusLabel.t
ext = status.length ? status : @"Unknown"
;
statusLabel.text
Color = statusColor
;
UILabel *statusLabel = [[UILabel alloc] init];
statusLabel.t
ranslatesAutoresizingMaskIntoConstraints = NO
;
statusLabel.text
= status
;
statusLabel.font = [UIFont boldSystemFontOfSize:15];
statusLabel.textColor = statusColor;
[card addSubview:statusLabel];
UILabel *dateLabel = [[UILabel alloc] init
WithFrame:CGRectMake(width - 130, 10, 118, 18)
];
dateLabel.t
extAlignment = NSTextAlignmentRight
;
UILabel *dateLabel = [[UILabel alloc] init];
dateLabel.t
ranslatesAutoresizingMaskIntoConstraints = 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;
...
...
framework/QmsPluginFramework/QmsPluginFramework/Dashboard/GeneralInfoViewController.mm
View file @
82cef0dc
...
...
@@ -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;
...
...
framework/QmsPluginFramework/QmsPluginFramework/Dashboard/ProjectViewController.mm
View file @
82cef0dc
...
...
@@ -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];
...
...
framework/QmsPluginFramework/QmsPluginFramework/Dashboard/UnitViewController.h
View file @
82cef0dc
...
...
@@ -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
framework/QmsPluginFramework/QmsPluginFramework/Dashboard/UnitViewController.mm
View file @
82cef0dc
...
...
@@ -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];
}
...
...
framework/QmsPluginFramework/QmsPluginFramework/Issue/AddInfoViewController.mm
View file @
82cef0dc
...
...
@@ -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];
}];
}
...
...
framework/QmsPluginFramework/QmsPluginFramework/Issue/AddIssue/DetailsViewController.mm
View file @
82cef0dc
...
...
@@ -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);
...
...
framework/QmsPluginFramework/QmsPluginFramework/Issue/AddIssue/PlanViewController.mm
View file @
82cef0dc
...
...
@@ -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;
...
...
framework/QmsPluginFramework/QmsPluginFramework/Issue/HistoryViewController.mm
View file @
82cef0dc
...
...
@@ -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;
...
...
framework/QmsPluginFramework/QmsPluginFramework/Issue/IssuesViewController.mm
View file @
82cef0dc
...
...
@@ -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;
...
...
framework/QmsPluginFramework/QmsPluginFramework/Issue/WithdrawIssuesViewController.mm
View file @
82cef0dc
...
...
@@ -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:1
0
0],
[searchContainer.heightAnchor constraintEqualToConstant:1
2
0],
[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"] ?: @[];
...
...
framework/QmsPluginFramework/QmsPluginFramework/Issue/WithdrawSingleIssueViewController.mm
View file @
82cef0dc
...
...
@@ -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;
...
...
framework/QmsPluginFramework/QmsPluginFramework/Sync/SyncProjectViewController.mm
View file @
82cef0dc
...
...
@@ -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];
...
...
framework/QmsPluginFramework/QmsPluginFramework/Utilities/API/APIClient.h
View file @
82cef0dc
...
...
@@ -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
...
...
framework/QmsPluginFramework/QmsPluginFramework/Utilities/API/APIClient.mm
View file @
82cef0dc
// 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(@"📦 [AddI
ssue
] Uploading %lu image(s)", (unsigned long)images.count);
NSLog(@"📦 [AddI
nfo
] Uploading %lu image(s)", (unsigned long)images.count);
NSURLSessionDataTask *task = [[NSURLSession sharedSession]
dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
NSLog(@"❌ [AddI
ssue
] Network error: %@", error);
dispatch_async(dispatch_get_main_queue(), ^{ if (completion) completion(nil, error); });
NSLog(@"❌ [AddI
nfo
] 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(@"⚠️ [AddI
ssue
] JSON parse error: %@\nRaw response: %@", jsonErr, raw);
dispatch_async(dispatch_get_main_queue(), ^{ if (completion) completion(nil, jsonErr); });
NSLog(@"⚠️ [AddI
nfo
] JSON parse error: %@\nRaw response: %@", jsonErr, raw);
dispatch_async(dispatch_get_main_queue(), ^{ if (completion) completion(
NO,
nil, jsonErr); });
return;
}
NSLog(@"✅ [AddI
ssue
] Response: %@", json);
dispatch_async(dispatch_get_main_queue(), ^{ if (completion) completion(json, nil); });
NSLog(@"✅ [AddI
nfo
] 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
framework/QmsPluginFramework/QmsPluginFramework/Utilities/API/APIConfig.h
View file @
82cef0dc
...
...
@@ -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
...
...
framework/QmsPluginFramework/QmsPluginFramework/Utilities/API/APIConfig.mm
View file @
82cef0dc
// 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
framework/QmsPluginFramework/QmsPluginFramework/Utilities/OfflineSync/OfflineSyncManager.mm
View file @
82cef0dc
...
...
@@ -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 *er
r) {
if (
err
) {
completion(nil, err);
[APIClient requestArrayImage:payload completion:^(
BOOL success, NSDictionary *res, NSError *erro
r) {
if (
!success
) {
completion(nil, err
or
);
return;
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment