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