Commit 6da6b554 authored by Wei Han's avatar Wei Han

code update

parent 9e06acac
......@@ -8,10 +8,10 @@
/* Begin PBXBuildFile section */
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
21E89413E97A865028E9F8E7 /* libPods-example.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B670364245CE8751F827745 /* libPods-example.a */; };
44006126336D83DB220E7214 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; };
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
92F3BF4D0A6D1113D359EFA3 /* libPods-example.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 358AE48123A5C7B504741D4F /* libPods-example.a */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
......@@ -19,12 +19,12 @@
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = example/Images.xcassets; sourceTree = "<group>"; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = example/Info.plist; sourceTree = "<group>"; };
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = example/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
521745100EAB4A9BA708C727 /* Pods-example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-example.debug.xcconfig"; path = "Target Support Files/Pods-example/Pods-example.debug.xcconfig"; sourceTree = "<group>"; };
358AE48123A5C7B504741D4F /* libPods-example.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-example.a"; sourceTree = BUILT_PRODUCTS_DIR; };
761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = example/AppDelegate.swift; sourceTree = "<group>"; };
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = example/LaunchScreen.storyboard; sourceTree = "<group>"; };
9B670364245CE8751F827745 /* libPods-example.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-example.a"; sourceTree = BUILT_PRODUCTS_DIR; };
9240166696F5B2EC6A36F688 /* Pods-example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-example.debug.xcconfig"; path = "Target Support Files/Pods-example/Pods-example.debug.xcconfig"; sourceTree = "<group>"; };
950B7AF3DD3B011B04438348 /* Pods-example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-example.release.xcconfig"; path = "Target Support Files/Pods-example/Pods-example.release.xcconfig"; sourceTree = "<group>"; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
F54AF3C297616CFF0E5CB079 /* Pods-example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-example.release.xcconfig"; path = "Target Support Files/Pods-example/Pods-example.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
......@@ -32,7 +32,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
21E89413E97A865028E9F8E7 /* libPods-example.a in Frameworks */,
92F3BF4D0A6D1113D359EFA3 /* libPods-example.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
......@@ -55,7 +55,7 @@
isa = PBXGroup;
children = (
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
9B670364245CE8751F827745 /* libPods-example.a */,
358AE48123A5C7B504741D4F /* libPods-example.a */,
);
name = Frameworks;
sourceTree = "<group>";
......@@ -92,8 +92,8 @@
BBD78D7AC51CEA395F1C20DB /* Pods */ = {
isa = PBXGroup;
children = (
521745100EAB4A9BA708C727 /* Pods-example.debug.xcconfig */,
F54AF3C297616CFF0E5CB079 /* Pods-example.release.xcconfig */,
9240166696F5B2EC6A36F688 /* Pods-example.debug.xcconfig */,
950B7AF3DD3B011B04438348 /* Pods-example.release.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
......@@ -105,13 +105,13 @@
isa = PBXNativeTarget;
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "example" */;
buildPhases = (
14A37E37235579F72338737B /* [CP] Check Pods Manifest.lock */,
F073E18940A3F2D5B6D5B315 /* [CP] Check Pods Manifest.lock */,
13B07F871A680F5B00A75B9A /* Sources */,
13B07F8C1A680F5B00A75B9A /* Frameworks */,
13B07F8E1A680F5B00A75B9A /* Resources */,
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
6F216721E9B6E9B3144F7620 /* [CP] Embed Pods Frameworks */,
42F37D61A8BF807876CD4F07 /* [CP] Copy Pods Resources */,
9E691098E1BDE21450BE0492 /* [CP] Embed Pods Frameworks */,
9554DF7B7A44E78B79E32173 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
......@@ -183,60 +183,60 @@
shellPath = /bin/sh;
shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n";
};
14A37E37235579F72338737B /* [CP] Check Pods Manifest.lock */ = {
9554DF7B7A44E78B79E32173 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-example/Pods-example-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-example-checkManifestLockResult.txt",
"${PODS_ROOT}/Target Support Files/Pods-example/Pods-example-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-example/Pods-example-resources.sh\"\n";
showEnvVarsInLog = 0;
};
42F37D61A8BF807876CD4F07 /* [CP] Copy Pods Resources */ = {
9E691098E1BDE21450BE0492 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-example/Pods-example-resources-${CONFIGURATION}-input-files.xcfilelist",
"${PODS_ROOT}/Target Support Files/Pods-example/Pods-example-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-example/Pods-example-resources-${CONFIGURATION}-output-files.xcfilelist",
"${PODS_ROOT}/Target Support Files/Pods-example/Pods-example-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-example/Pods-example-resources.sh\"\n";
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-example/Pods-example-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
6F216721E9B6E9B3144F7620 /* [CP] Embed Pods Frameworks */ = {
F073E18940A3F2D5B6D5B315 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-example/Pods-example-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-example/Pods-example-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-example-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-example/Pods-example-frameworks.sh\"\n";
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
......@@ -255,7 +255,7 @@
/* Begin XCBuildConfiguration section */
13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 521745100EAB4A9BA708C727 /* Pods-example.debug.xcconfig */;
baseConfigurationReference = 9240166696F5B2EC6A36F688 /* Pods-example.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
......@@ -283,7 +283,7 @@
};
13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = F54AF3C297616CFF0E5CB079 /* Pods-example.release.xcconfig */;
baseConfigurationReference = 950B7AF3DD3B011B04438348 /* Pods-example.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
......
......@@ -8,32 +8,32 @@
<key>BinaryPath</key>
<string>QmsPluginFramework.framework/QmsPluginFramework</string>
<key>LibraryIdentifier</key>
<string>ios-arm64_x86_64-simulator</string>
<string>ios-arm64</string>
<key>LibraryPath</key>
<string>QmsPluginFramework.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>simulator</string>
</dict>
<dict>
<key>BinaryPath</key>
<string>QmsPluginFramework.framework/QmsPluginFramework</string>
<key>LibraryIdentifier</key>
<string>ios-arm64</string>
<string>ios-arm64_x86_64-simulator</string>
<key>LibraryPath</key>
<string>QmsPluginFramework.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>simulator</string>
</dict>
</array>
<key>CFBundlePackageType</key>
......
......@@ -512,7 +512,7 @@
return;
}
[APIClient fetchCancelAppointment:self.appointmentData[@"appointment_id"]
[APIClient requestCancelAppointment:self.appointmentData[@"appointment_id"]
reason:remarks
completion:^(NSDictionary *data, NSError *error)
{
......
......@@ -285,7 +285,7 @@
- (void)viewDidLoad {
[super viewDidLoad];
[self fetchAccessItem];
[self requestAccessItem];
self.view.backgroundColor = UIColor.whiteColor;
[self setupHeader];
......@@ -361,7 +361,7 @@
__weak typeof(self) weakSelf = self;
[self.tableView addPullToRefresh:^(UIRefreshControl *refresh) {
[weakSelf fetchAccessItem];
[weakSelf requestAccessItem];
}];
[self.view addSubview:self.tableView];
......@@ -375,8 +375,8 @@
}
#pragma mark - Api
- (void)fetchAccessItem{
[APIClient fetchAccessItem:@"135"
- (void)requestAccessItem{
[APIClient requestAccessItem:@"135"
completion:^(BOOL success, NSDictionary *data, NSError *error) {
NSLog(@"📦 Received General Info Data: %@", data);
......
......@@ -43,7 +43,7 @@
[super viewDidLoad];
self.view.backgroundColor = UIColor.whiteColor;
[self fetchGeneralInfo];
[self requestGeneralInfo];
[self setupNavBar];
[self setupUnitInfoHeader];
[self setupTabsBar];
......@@ -931,7 +931,7 @@
return;
}
[APIClient fetchCancelAppointment:self.appointmentData[@"id"]
[APIClient requestCancelAppointment:self.appointmentData[@"id"]
reason:remarks
completion:^(NSDictionary *data, NSError *error)
{
......@@ -965,8 +965,8 @@
}];
}
-(void) fetchGeneralInfo {
[APIClient fetchGeneralInfo:^(NSDictionary *data, NSError *error) {
-(void) requestGeneralInfo {
[APIClient requestGeneralInfo:^(NSDictionary *data, NSError *error) {
if (error) return;
NSDictionary *generalInfo = data[@"Data"];
self.floorPlanImage = generalInfo[@"image"];
......
......@@ -7,6 +7,7 @@
#import "CalendarView.h"
#import "NSDate+CalendarHelpers.h"
#import "AppointmentTimeSlotViewController.h"
#import "PullToRefresh.h"
@interface AppointmentViewController () <WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate, CalendarViewDelegate>
......@@ -58,23 +59,71 @@
@implementation AppointmentViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = UIColor.whiteColor;
[self setupUI];
[self setupTermsOverlay];
[self fetchAppointment];
[self fetchGeneralInfo];
[super viewDidLoad];
self.view.backgroundColor = UIColor.whiteColor;
[self setupHeader];
[self setupFooter];
[self setupUI];
}
#pragma mark - Setup UI
- (void)setupUI {
[self setupHeader];
[self setupLabels];
[self setupCalendarSection];
[self setupFooter];
[self setupTutorialOverlay];
// Scroll view
self.scrollView = [[UIScrollView alloc] init];
self.scrollView.translatesAutoresizingMaskIntoConstraints = NO;
self.scrollView.alwaysBounceVertical = YES;
[self.view addSubview:self.scrollView];
self.contentView = [[UIView alloc] init];
self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
[self.scrollView addSubview:self.contentView];
UILayoutGuide *safe = self.view.safeAreaLayoutGuide;
[NSLayoutConstraint activateConstraints:@[
[self.scrollView.topAnchor constraintEqualToAnchor:self.headerBar.bottomAnchor],
[self.scrollView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[self.scrollView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[self.scrollView.bottomAnchor constraintEqualToAnchor:self.footerView.topAnchor],
[self.contentView.topAnchor constraintEqualToAnchor:self.scrollView.topAnchor],
[self.contentView.leadingAnchor constraintEqualToAnchor:self.scrollView.leadingAnchor],
[self.contentView.trailingAnchor constraintEqualToAnchor:self.scrollView.trailingAnchor],
[self.contentView.bottomAnchor constraintEqualToAnchor:self.scrollView.bottomAnchor],
[self.contentView.widthAnchor constraintEqualToAnchor:self.scrollView.widthAnchor],
]];
// Create labels, calendar, legend
[self setupLabels:self.contentView];
[self setupCalendarSection:self.contentView];
// Optional: wrap in stack if you want
UIStackView *contentStack = [[UIStackView alloc] initWithArrangedSubviews:@[self.unitLabel, self.unitName, self.stepLabel, self.calendarContainer, self.legendStack]];
contentStack.axis = UILayoutConstraintAxisVertical;
contentStack.spacing = 16;
contentStack.translatesAutoresizingMaskIntoConstraints = NO;
[self.contentView addSubview:contentStack];
[NSLayoutConstraint activateConstraints:@[
[contentStack.topAnchor constraintEqualToAnchor:self.contentView.topAnchor constant:16],
[contentStack.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor constant:16],
[contentStack.trailingAnchor constraintEqualToAnchor:self.contentView.trailingAnchor constant:-16],
[contentStack.bottomAnchor constraintEqualToAnchor:self.contentView.bottomAnchor constant:-16]
]];
// Tutorial overlay added last so it’s on top
[self setupTutorialOverlay];
[self setupTermsOverlay];
// Pull-to-refresh
__weak typeof(self) weakSelf = self;
[self.scrollView addPullToRefresh:^(UIRefreshControl *refreshControl) {
[weakSelf refreshAppointmentData:refreshControl];
}];
}
#pragma mark - Navigation Bar
- (void)setupHeader {
......@@ -154,7 +203,11 @@
self.termsWebView.scrollView.delegate = self;
self.termsWebView.navigationDelegate = self;
[self.termsContainer addSubview:self.termsWebView];
self.termsContainer.backgroundColor = [UIColor yellowColor];
self.termsWebView.backgroundColor = [UIColor redColor];
// Agree button
self.agreeButton = [UIButton buttonWithType:UIButtonTypeSystem];
self.agreeButton.translatesAutoresizingMaskIntoConstraints = NO;
......@@ -192,22 +245,20 @@
#pragma mark - Labels
- (void)setupLabels {
UILayoutGuide *safe = self.view.safeAreaLayoutGuide;
- (void)setupLabels:(UIView *)parent {
self.unitLabel = [[UILabel alloc] init];
self.unitLabel.text = @"For Unit No.:";
self.unitLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightThin];
self.unitLabel.textColor = [UIColor grayColor];
self.unitLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.unitLabel];
[parent addSubview:self.unitLabel];
self.unitName = [[UILabel alloc] init];
self.unitName.text = [NSString stringWithFormat:@"%@, %@", self.projectName, self.projectCode];
self.unitName.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
self.unitName.textColor = [UIColor blackColor];
self.unitName.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.unitName];
[parent addSubview:self.unitName];
self.stepLabel = [[UILabel alloc] init];
self.stepLabel.text = @"Step 1/3: Select your preferred date";
......@@ -218,31 +269,31 @@
self.stepLabel.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0];
self.stepLabel.layer.cornerRadius = 6.0;
self.stepLabel.clipsToBounds = YES;
[self.view addSubview:self.stepLabel];
[parent addSubview:self.stepLabel];
[NSLayoutConstraint activateConstraints:@[
[self.unitLabel.topAnchor constraintEqualToAnchor:self.headerBar.bottomAnchor constant:16],
[self.unitLabel.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:16],
[self.unitLabel.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-16],
[self.unitLabel.topAnchor constraintEqualToAnchor:parent.topAnchor constant:16],
[self.unitLabel.leadingAnchor constraintEqualToAnchor:parent.leadingAnchor constant:16],
[self.unitLabel.trailingAnchor constraintEqualToAnchor:parent.trailingAnchor constant:-16],
[self.unitName.topAnchor constraintEqualToAnchor:self.unitLabel.bottomAnchor constant:8],
[self.unitName.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:16],
[self.unitName.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-16],
[self.unitName.leadingAnchor constraintEqualToAnchor:parent.leadingAnchor constant:16],
[self.unitName.trailingAnchor constraintEqualToAnchor:parent.trailingAnchor constant:-16],
[self.stepLabel.topAnchor constraintEqualToAnchor:self.unitName.bottomAnchor constant:16],
[self.stepLabel.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:16],
[self.stepLabel.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-16],
[self.stepLabel.leadingAnchor constraintEqualToAnchor:parent.leadingAnchor constant:16],
[self.stepLabel.trailingAnchor constraintEqualToAnchor:parent.trailingAnchor constant:-16],
[self.stepLabel.heightAnchor constraintEqualToConstant:30]
]];
}
#pragma mark - Calendar Section
- (void)setupCalendarSection {
- (void)setupCalendarSection:(UIView *)parent {
self.calendarContainer = [[UIView alloc] init];
self.calendarContainer.backgroundColor = [UIColor whiteColor];
self.calendarContainer.layer.cornerRadius = 12.0;
self.calendarContainer.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.calendarContainer];
[parent addSubview:self.calendarContainer];
self.calendarView = [[CalendarView alloc] initWithFrame:CGRectZero];
self.calendarView.translatesAutoresizingMaskIntoConstraints = NO;
......@@ -256,7 +307,7 @@
self.legendStack.alignment = UIStackViewAlignmentCenter;
self.legendStack.distribution = UIStackViewDistributionEqualSpacing;
self.legendStack.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.legendStack];
[parent addSubview:self.legendStack];
NSArray *legendData = @[
@{@"title": @"Available", @"color": UIColor.systemGreenColor},
......@@ -285,13 +336,13 @@
CGFloat w = UIScreen.mainScreen.bounds.size.width - 16 - 16 - 12 - 12; // container + inner padding
CGFloat cell = floor(w / 7.0);
CGFloat rows = 6; // max rows calendar ever needs
CGFloat rows = 6; // max rows
CGFloat calendarHeight = 40 + 28 + (cell * 0.9 + 12) * rows + 12 + 20;
[NSLayoutConstraint activateConstraints:@[
[self.calendarContainer.topAnchor constraintEqualToAnchor:self.stepLabel.bottomAnchor constant:16],
[self.calendarContainer.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:16],
[self.calendarContainer.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-16],
[self.calendarContainer.leadingAnchor constraintEqualToAnchor:parent.leadingAnchor constant:16],
[self.calendarContainer.trailingAnchor constraintEqualToAnchor:parent.trailingAnchor constant:-16],
[self.calendarContainer.heightAnchor constraintEqualToConstant:calendarHeight],
[self.calendarView.topAnchor constraintEqualToAnchor:self.calendarContainer.topAnchor constant:12],
......@@ -300,7 +351,7 @@
[self.calendarView.bottomAnchor constraintEqualToAnchor:self.calendarContainer.bottomAnchor constant:-12],
[self.legendStack.topAnchor constraintEqualToAnchor:self.calendarContainer.bottomAnchor constant:16],
[self.legendStack.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
[self.legendStack.centerXAnchor constraintEqualToAnchor:parent.centerXAnchor],
]];
}
......@@ -398,8 +449,8 @@
}
#pragma mark - api
- (void)fetchAppointment {
[APIClient fetchAppointment:^(NSDictionary *data, NSError *error) {
- (void)requestAppointment {
[APIClient requestAppointment:^(NSDictionary *data, NSError *error) {
if (error) return;
if (!data[@"Data"]) return;
......@@ -430,15 +481,17 @@
NSString *termsHTML = selectedTypeDict[@"terms"] ?: @"";
NSString *styledHTML = [NSString stringWithFormat:
@"<html>"
"<head>"
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0\">"
"<style>"
"body { font-size: 14px; font-family: -apple-system; line-height: 1.5; padding: 12px; background-color: #F9F9F9; }"
"p { margin-bottom: 12px; }"
"</style>"
"</head>"
"<body>%@</body>"
"</html>", termsHTML];
"<head>"
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0\">"
"<style>"
"body { font-size: 14px; font-family: -apple-system; line-height: 1.5; padding: 12px; background-color: #F9F9F9; color: #000000; }"
"p, div, span, li { color: #000000 !important; }"
"p { margin-bottom: 12px; }"
"</style>"
"</head>"
"<body>%@</body>"
"</html>", termsHTML];
self.termsAndCondition = styledHTML;
......@@ -450,8 +503,8 @@
}];
}
-(void) fetchGeneralInfo {
[APIClient fetchGeneralInfo:^(NSDictionary *data, NSError *error) {
-(void) requestGeneralInfo {
[APIClient requestGeneralInfo:^(NSDictionary *data, NSError *error) {
if (error) return;
NSDictionary *generalInfo = data[@"Data"];
......@@ -575,10 +628,16 @@
- (void)showTermsOverlay {
self.termsOverlay.hidden = NO;
[self.view bringSubviewToFront:self.termsOverlay];
self.termsOverlay.alpha = 0.0;
[UIView animateWithDuration:0.25 animations:^{
self.termsOverlay.alpha = 1.0;
} completion:^(BOOL finished) {
// Ensure webview loads now
if (self.termsAndCondition) {
[self.termsWebView loadHTMLString:self.termsAndCondition baseURL:nil];
}
}];
}
......@@ -642,5 +701,44 @@
[self presentViewController:alert animated:YES completion:nil];
}
- (void)refreshAppointmentData:(UIRefreshControl *)refreshControl {
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
[APIClient requestAppointment:^(NSDictionary *data, NSError *error) {
if (data[@"Data"]) {
NSDictionary *dataDict = data[@"Data"];
self.appointmentData = dataDict;
NSString *termsHTML = dataDict[self.appointmentType][@"terms"] ?: @"";
NSString *styledHTML = [NSString stringWithFormat:
@"<html><head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
"<style>body { font-size:14px; font-family:-apple-system; padding:12px; }</style>"
"</head><body>%@</body></html>", termsHTML];
self.termsAndCondition = styledHTML;
dispatch_async(dispatch_get_main_queue(), ^{
[self.termsWebView loadHTMLString:styledHTML baseURL:nil];
});
NSArray *slots = dataDict[self.appointmentType][@"slots"] ?: @[];
self.allSlots = slots;
[self buildMarkedDatesFromSlots:slots];
[self.calendarView.collectionView reloadData];
}
dispatch_group_leave(group);
}];
dispatch_group_enter(group);
[APIClient requestGeneralInfo:^(NSDictionary *data, NSError *error) {
id reps = data[@"Data"][@"representative"];
self.representativeList = [reps isKindOfClass:[NSArray class]] ? reps : @[];
dispatch_group_leave(group);
}];
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
[self.scrollView endPullToRefresh];
});
}
@end
......@@ -138,7 +138,7 @@
[self setupListingView];
[self setupFilterOverlay];
[self setupLoadingOverlay];
[self fetchDashboardInfo];
[self requestDashboardInfo];
}
#pragma mark - Top Bar
......@@ -679,7 +679,7 @@
}
- (void)onRefresh {
[self fetchDashboardInfo];
[self requestDashboardInfo];
}
#pragma mark - Bottom Button
......@@ -924,10 +924,10 @@
}
#pragma mark - api
- (void)fetchDashboardInfo {
- (void)requestDashboardInfo {
[self.loadingView startAnimating];
[APIClient fetchDashboardInfo:^(NSDictionary *data, NSError *error) {
[APIClient requestDashboardInfo:^(NSDictionary *data, NSError *error) {
[self.loadingView stopAnimating];
......
......@@ -12,6 +12,7 @@
#import "GeneralInfoViewController.h"
#import "AccessItemViewController.h"
#import "OfflineSyncManager.h"
#import "LocalStorage.h"
@interface DashboardViewController ()
......@@ -81,7 +82,6 @@
NSLog(@"dashboard.drawerController = %@", self.drawerController);
// [[OfflineSyncManager shared] clearOfflineQueue];
}
- (void)viewWillAppear:(BOOL)animated {
......@@ -113,13 +113,13 @@
__block NSDictionary *announcementData = nil;
dispatch_group_enter(group);
[APIClient fetchDashboardInfo:^(NSDictionary *data, NSError *error) {
[APIClient requestDashboardInfo:^(NSDictionary *data, NSError *error) {
if (!error) dashboardData = data;
dispatch_group_leave(group);
}];
dispatch_group_enter(group);
[APIClient fetchAnnouncement:^(NSDictionary *data, NSError *error) {
[APIClient requestAnnouncement:^(NSDictionary *data, NSError *error) {
if (!error) announcementData = data;
dispatch_group_leave(group);
}];
......@@ -593,7 +593,7 @@
NSDictionary *appData = data[@"AppData"];
if (![appData[@"status"] isEqualToString:@"success"]) {
NSLog(@"Announcement fetch failed: %@", appData[@"message"]);
NSLog(@"Announcement request failed: %@", appData[@"message"]);
return;
}
......
......@@ -83,7 +83,7 @@ typedef struct {
self.documentsContainer.clipsToBounds = YES;
self.formsContainer.clipsToBounds = YES;
[self fetchGeneralInfo];
[self requestGeneralInfo];
[self setupTopBar];
[self setupTabControl];
[self setupScrollView];
......@@ -540,8 +540,8 @@ typedef struct {
}
#pragma mark - api
- (void)fetchGeneralInfo {
[APIClient fetchGeneralInfo:^(NSDictionary *data, NSError *error) {
- (void)requestGeneralInfo {
[APIClient requestGeneralInfo:^(NSDictionary *data, NSError *error) {
if (error || !data) {
NSLog(@"❌ General info error: %@", error.localizedDescription);
return;
......
......@@ -67,11 +67,11 @@
}
- (void)handleGetUnitPlan {
NSLog(@"🌐 Fetching Unit Plan data...");
NSLog(@"🌐 requesting Unit Plan data...");
[APIClient fetchGetUnitPlan:^(NSDictionary *data, NSError *error) {
[APIClient requestGetUnitPlan:^(NSDictionary *data, NSError *error) {
if (error) {
NSLog(@"❌ Unit Plan fetch failed: %@", error.localizedDescription);
NSLog(@"❌ Unit Plan request failed: %@", error.localizedDescription);
return;
}
......@@ -165,7 +165,6 @@
}];
}
- (void)highlightExistingIssueAtCoordinates:(CGFloat)x Y:(CGFloat)y {
NSLog(@"📍 Highlighting existing issue at (%.2f, %.2f)", x, y);
......@@ -702,20 +701,19 @@
}
if (self.locationID) {
NSLog(@"📡 Fetching settings for location %@ and plan %@", self.locationID, self.planID);
NSLog(@"📡 requesting settings for location %@ and plan %@", self.locationID, self.planID);
[self handleSettingsByLocation:self.locationID planID:self.planID];
} else {
NSLog(@"⚠️ No match found for %@", self.selectedLocation);
}
}
- (void)handleSettingsByLocation:(NSString *)locationID planID:(NSString *)planID {
[APIClient fetchSettingsByLocation:locationID
[APIClient requestSettingsByLocation:locationID
projectID:@"8"
completion:^(NSDictionary *data, NSError *error) {
if (error) {
NSLog(@"❌ Error fetching settings: %@", error);
NSLog(@"❌ Error requesting settings: %@", error);
return;
}
......
......@@ -126,10 +126,10 @@
#pragma mark - API request/response
- (void)handleIssues {
[APIClient fetchHistory:self.issueID
[APIClient requestHistory:self.issueID
completion:^(NSDictionary *data, NSError *error) {
if (error) {
NSLog(@"❌ Error fetching history: %@", error);
NSLog(@"❌ Error requesting history: %@", error);
return;
}
......
......@@ -298,11 +298,11 @@
#pragma mark - Networking
- (void)handleGetIssue {
NSLog(@"🌐 Fetching Issue data...");
NSLog(@"🌐 requesting Issue data...");
[APIClient fetchIssues:^(NSDictionary *data, NSError *error) {
[APIClient requestIssues:^(NSDictionary *data, NSError *error) {
if (error) {
NSLog(@"❌ Issue fetch failed: %@", error.localizedDescription);
NSLog(@"❌ Issue request failed: %@", error.localizedDescription);
return;
}
......
......@@ -174,7 +174,7 @@
[self setupSearchBar];
[self setupTableView];
[self setupBottomBar];
[self fetchIssues];
[self requestIssues];
}
- (void)viewWillAppear:(BOOL)animated {
......@@ -382,8 +382,8 @@
}
#pragma mark - Data
- (void)fetchIssues {
[APIClient fetchIssues:^(NSDictionary *data, NSError *error) {
- (void)requestIssues {
[APIClient requestIssues:^(NSDictionary *data, NSError *error) {
if (error) return;
NSArray *allIssues = data[@"Data"] ?: @[];
......
......@@ -264,7 +264,7 @@
- (void)requestDeleteIssue:(NSString *)issueID remarks:(NSString *)remarks {
NSLog(@"🚀 Sending DELETE ISSUE for ID=%@, remarks=%@", issueID, remarks);
[APIClient fetchDeleteIssue:issueID remarks:remarks completion:^(NSDictionary *data, NSError *error) {
[APIClient requestDeleteIssue:issueID remarks:remarks completion:^(NSDictionary *data, NSError *error) {
if (error) {
NSLog(@"Error %@", error.localizedDescription);
return;
......@@ -301,7 +301,7 @@
-(void)requestVoidIssue:(NSArray *)ids remarks:(NSString *)remarks {
NSLog(@"🚀 Sending VOID ISSUE for ID=%@, remarks=%@", self.selectedIssues, remarks);
[APIClient fetchVoidIssue:ids
[APIClient requestVoidIssue:ids
remarks:remarks
completion:^(NSDictionary *data, NSError *error) {
if (error) {
......
//// SyncEngine.h
//
//#import <Foundation/Foundation.h>
//@interface SyncEngine : NSObject
//
//@property (nonatomic, assign) BOOL submitPending;
//@property (nonatomic, assign) BOOL downloadUnits;
//
//- (void)startWithProgress:(SyncProgressBlock)progress
// completion:(SyncCompletionBlock)completion;
//
//@end
//
//// SyncEngine.mm
//
//#import "SyncEngine.h"
//#import "APIClient.h"
//#import "NetworkManager.h"
//
//@interface SyncEngine ()
//
//// Callbacks
//@property (nonatomic, copy) SyncProgressBlock progressBlock;
//@property (nonatomic, copy) SyncCompletionBlock completionBlock;
//
//// Progress tracking
//@property (nonatomic, assign) NSInteger currentStep;
//@property (nonatomic, assign) NSInteger totalSteps;
//
//@end
//
//@implementation SyncEngine
//
//#pragma mark - Public Entry Point
//
//- (void)startWithProgress:(SyncProgressBlock)progress
// completion:(SyncCompletionBlock)completion {
//
// self.progressBlock = progress;
// self.completionBlock = completion;
//
// self.currentStep = 0;
// self.totalSteps = [self calculateTotalSteps];
//
// [self stepCheckNetwork];
//}
//
//#pragma mark - Step Count
//
//- (NSInteger)calculateTotalSteps {
// NSInteger steps = 1; // check network
//
// if (self.submitPending) {
// steps += 1; // submit pending
// }
//
// if (self.downloadUnits) {
// steps += 1; // download data
// }
//
// steps += 1; // finalize
//
// return steps;
//}
//
//#pragma mark - Step 1: Network Check
//
//- (void)stepCheckNetwork {
// [self reportProgress:@"Checking network"];
//
// if (![NetworkManager.sharedManager isConnected]) {
// NSError *error =
// [NSError errorWithDomain:@"Sync"
// code:-1009
// userInfo:@{
// NSLocalizedDescriptionKey :
// @"No internet connection"
// }];
// [self finish:NO error:error];
// return;
// }
//
// if (self.submitPending) {
// [self stepSubmitPending];
// } else if (self.downloadUnits) {
// [self stepDownloadCommonAreas];
// } else {
// [self stepFinalize];
// }
//}
//
//#pragma mark - Step 2: Submit Pending Offline Items
//
//- (void)stepSubmitPending {
// [self reportProgress:@"Submitting pending items"];
//
// __weak typeof(self) weakSelf = self;
//
// [self submitPendingItemsWithCompletion:^(BOOL success, NSError *error) {
// __strong typeof(weakSelf) self = weakSelf;
//
// if (!success) {
// [self finish:NO error:error];
// return;
// }
//
// if (self.downloadUnits) {
// [self stepDownloadCommonAreas];
// } else {
// [self stepFinalize];
// }
// }];
//}
//
//#pragma mark - Step 3: Download Data
//
//- (void)stepDownloadCommonAreas {
// [self reportProgress:@"Downloading common areas"];
//
// __weak typeof(self) weakSelf = self;
//
// [APIClient fetchCommonArea:^(NSDictionary *data, NSError *error) {
// __strong typeof(weakSelf) self = weakSelf;
//
// if (error) {
// [self finish:NO error:error];
// return;
// }
//
// [self persistCommonAreas:data];
// [self stepFinalize];
// }];
//}
//
//#pragma mark - Step 4: Finalize
//
//- (void)stepFinalize {
// [self reportProgress:@"Finalizing sync"];
// [self finalizeSync];
// [self finish:YES error:nil];
//}
//
//#pragma mark - Progress & Completion Helpers
//
//- (void)reportProgress:(NSString *)message {
// self.currentStep++;
//
// if (self.progressBlock) {
// self.progressBlock(self.currentStep,
// self.totalSteps,
// message);
// }
//}
//
//- (void)finish:(BOOL)success error:(NSError *)error {
// if (self.completionBlock) {
// self.completionBlock(success, error);
// }
//
// // Clean up references
// self.progressBlock = nil;
// self.completionBlock = nil;
//}
//
//#pragma mark - Internal Hooks (Implement These)
//
//- (void)submitPendingItemsWithCompletion:(void (^)(BOOL success, NSError *error))completion {
// // TODO:
// // - Query offline table
// // - Upload ONE item at a time
// // - Update local DB on success/failure
// // - Recursively continue until empty
// // - Call completion when done
//}
//
//- (void)persistCommonAreas:(NSDictionary *)data {
// // TODO:
// // Save common areas into local storage / DB
//}
//
//- (void)finalizeSync {
// // TODO:
// // - Update last sync timestamp
// // - Persist sync success state
//}
//
//@end
// SyncHistoryViewController.mm
#import "SyncHistoryViewController.h"
#import "OfflineSyncManager.h"
typedef NS_ENUM(NSInteger, HistorySyncStatus) {
HistorySyncStatusSuccess,
......@@ -14,6 +15,7 @@ typedef NS_ENUM(NSInteger, HistorySyncStatus) {
@property (nonatomic, strong) NSString *uploadedOn;
@property (nonatomic, strong) NSString *actionMade;
@property (nonatomic, assign) HistorySyncStatus status;
@property (nonatomic, strong) NSDictionary *raw;
@end
@implementation HistorySyncItem
......@@ -139,7 +141,6 @@ typedef NS_ENUM(NSInteger, HistorySyncStatus) {
@property (nonatomic, strong) NSArray<HistorySyncItem *> *filteredItems;
@property (nonatomic, assign) NSInteger selectedTab;
@end
@implementation SyncHistoryViewController
......@@ -157,7 +158,13 @@ typedef NS_ENUM(NSInteger, HistorySyncStatus) {
[self setupEmptyState];
[self setupBottomButton];
[self loadMockData];
[self loadHistoryData];
[self applyFilter];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self loadHistoryData];
[self applyFilter];
}
......@@ -185,18 +192,18 @@ typedef NS_ENUM(NSInteger, HistorySyncStatus) {
self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self.headerView addSubview:self.titleLabel];
self.shareButton = [UIButton buttonWithType:UIButtonTypeSystem];
[self.shareButton setTitle:@"Share to Email" forState:UIControlStateNormal];
[self.shareButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
self.shareButton.layer.borderColor = UIColor.whiteColor.CGColor;
self.shareButton.layer.borderWidth = 0.5;
self.shareButton.layer.cornerRadius = 14;
[self.shareButton addTarget:self
action:@selector(onShare)
forControlEvents:UIControlEventTouchUpInside];
self.shareButton.titleLabel.font = [UIFont systemFontOfSize:14];
self.shareButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.headerView addSubview:self.shareButton];
// self.shareButton = [UIButton buttonWithType:UIButtonTypeSystem];
// [self.shareButton setTitle:@"Share to Email" forState:UIControlStateNormal];
// [self.shareButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
// self.shareButton.layer.borderColor = UIColor.whiteColor.CGColor;
// self.shareButton.layer.borderWidth = 0.5;
// self.shareButton.layer.cornerRadius = 14;
// [self.shareButton addTarget:self
// action:@selector(onShare)
// forControlEvents:UIControlEventTouchUpInside];
// self.shareButton.titleLabel.font = [UIFont systemFontOfSize:14];
// self.shareButton.translatesAutoresizingMaskIntoConstraints = NO;
// [self.headerView addSubview:self.shareButton];
[NSLayoutConstraint activateConstraints:@[
[self.headerView.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor],
......@@ -212,10 +219,10 @@ typedef NS_ENUM(NSInteger, HistorySyncStatus) {
[self.titleLabel.centerXAnchor constraintEqualToAnchor:self.headerView.centerXAnchor],
[self.titleLabel.centerYAnchor constraintEqualToAnchor:self.headerView.centerYAnchor],
[self.shareButton.trailingAnchor constraintEqualToAnchor:self.headerView.trailingAnchor constant:-8],
[self.shareButton.centerYAnchor constraintEqualToAnchor:self.headerView.centerYAnchor],
[self.shareButton.heightAnchor constraintEqualToConstant:28],
[self.shareButton.widthAnchor constraintEqualToConstant:120],
// [self.shareButton.trailingAnchor constraintEqualToAnchor:self.headerView.trailingAnchor constant:-8],
// [self.shareButton.centerYAnchor constraintEqualToAnchor:self.headerView.centerYAnchor],
// [self.shareButton.heightAnchor constraintEqualToConstant:28],
// [self.shareButton.widthAnchor constraintEqualToConstant:120],
]];
}
......@@ -312,6 +319,43 @@ typedef NS_ENUM(NSInteger, HistorySyncStatus) {
]];
}
#pragma mark - Data
- (void)loadHistoryData {
NSArray *sections =
[[OfflineSyncManager shared] historyItemsForUI];
[self dumpHistoryToFile:sections];
NSMutableArray *items = [NSMutableArray array];
for (NSDictionary *section in sections) {
NSArray *rows = section[@"items"];
for (NSDictionary *row in rows) {
HistorySyncItem *item = [HistorySyncItem new];
item.unitName = row[@"unitName"];
item.createdBy = row[@"createdBy"];
item.createdOn = row[@"createdOn"];
item.uploadedOn = row[@"uploadedOn"];
item.actionMade = row[@"type"];
NSString *status = row[@"status"];
item.status =
[status isEqualToString:@"FAILED"]
? HistorySyncStatusFailed
: HistorySyncStatusSuccess;
item.raw = row;
[items addObject:item];
}
}
self.allItems = items;
}
// Dummy data
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.filteredItems.count;
......@@ -383,6 +427,10 @@ typedef NS_ENUM(NSInteger, HistorySyncStatus) {
[self.syncAgainButton setTitle:@"SYNC AGAIN" forState:UIControlStateNormal];
[self.syncAgainButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
self.syncAgainButton.enabled = NO;
[self.syncAgainButton addTarget:self
action:@selector(onSyncAgain)
forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.syncAgainButton];
......@@ -472,5 +520,45 @@ typedef NS_ENUM(NSInteger, HistorySyncStatus) {
[self.tableView reloadData];
}
- (void)onSyncAgain {
for (HistorySyncItem *item in self.allItems) {
if (item.status == HistorySyncStatusFailed) {
[[OfflineSyncManager shared] retryHistoryItem:item.raw];
}
}
NSLog(@"Starting sync again");
// [[OfflineSyncManager shared] startManualSync];
[self loadHistoryData];
[self applyFilter];
}
- (void)dumpHistoryToFile:(id)history {
NSString *docDir =
NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask,
YES).firstObject;
NSString *path =
[docDir stringByAppendingPathComponent:@"sync_history_dump.txt"];
NSString *content =
[NSString stringWithFormat:@"=== SYNC HISTORY DUMP ===\n%@\n",
history];
NSError *error = nil;
BOOL success =
[content writeToFile:path
atomically:YES
encoding:NSUTF8StringEncoding
error:&error];
if (success) {
NSLog(@"📄 History dumped to: %@", path);
} else {
NSLog(@"❌ Failed to dump history: %@", error);
}
}
@end
......@@ -195,18 +195,18 @@ typedef NS_ENUM(NSInteger, SyncPendingViewMode) {
self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self.headerView addSubview:self.titleLabel];
self.shareButton = [UIButton buttonWithType:UIButtonTypeSystem];
[self.shareButton setTitle:@"Share to Email" forState:UIControlStateNormal];
[self.shareButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
self.shareButton.layer.borderColor = UIColor.whiteColor.CGColor;
self.shareButton.layer.borderWidth = 0.5;
self.shareButton.layer.cornerRadius = 14;
[self.shareButton addTarget:self
action:@selector(onShare)
forControlEvents:UIControlEventTouchUpInside];
self.shareButton.titleLabel.font = [UIFont systemFontOfSize:14];
self.shareButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.headerView addSubview:self.shareButton];
// self.shareButton = [UIButton buttonWithType:UIButtonTypeSystem];
// [self.shareButton setTitle:@"Share to Email" forState:UIControlStateNormal];
// [self.shareButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
// self.shareButton.layer.borderColor = UIColor.whiteColor.CGColor;
// self.shareButton.layer.borderWidth = 0.5;
// self.shareButton.layer.cornerRadius = 14;
// [self.shareButton addTarget:self
// action:@selector(onShare)
// forControlEvents:UIControlEventTouchUpInside];
// self.shareButton.titleLabel.font = [UIFont systemFontOfSize:14];
// self.shareButton.translatesAutoresizingMaskIntoConstraints = NO;
// [self.headerView addSubview:self.shareButton];
[NSLayoutConstraint activateConstraints:@[
[self.headerView.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor],
......@@ -222,10 +222,10 @@ typedef NS_ENUM(NSInteger, SyncPendingViewMode) {
[self.titleLabel.centerXAnchor constraintEqualToAnchor:self.headerView.centerXAnchor],
[self.titleLabel.centerYAnchor constraintEqualToAnchor:self.headerView.centerYAnchor],
[self.shareButton.trailingAnchor constraintEqualToAnchor:self.headerView.trailingAnchor constant:-8],
[self.shareButton.centerYAnchor constraintEqualToAnchor:self.headerView.centerYAnchor],
[self.shareButton.heightAnchor constraintEqualToConstant:28],
[self.shareButton.widthAnchor constraintEqualToConstant:120],
// [self.shareButton.trailingAnchor constraintEqualToAnchor:self.headerView.trailingAnchor constant:-8],
// [self.shareButton.centerYAnchor constraintEqualToAnchor:self.headerView.centerYAnchor],
// [self.shareButton.heightAnchor constraintEqualToConstant:28],
// [self.shareButton.widthAnchor constraintEqualToConstant:120],
]];
}
......
......@@ -14,7 +14,7 @@ typedef NS_ENUM(NSInteger, SyncProjectScreenState) {
static NSString * const kSyncSelectedUnitsKey = @"SYNC_SELECTEDUNITS";
static NSString * const kProjectListKey = @"PROJECT_LIST";
@interface SyncUnit : NSObject
@interface SyncUnit : NSObject <NSSecureCoding>
@property (nonatomic, strong) NSString *planId;
@property (nonatomic, strong) NSString *displayName;
@property (nonatomic, strong) NSDictionary *project;
......@@ -24,6 +24,33 @@ static NSString * const kProjectListKey = @"PROJECT_LIST";
@end
@implementation SyncUnit
+ (BOOL)supportsSecureCoding {
return YES;
}
- (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeObject:self.planId forKey:@"planId"];
[coder encodeObject:self.displayName forKey:@"displayName"];
[coder encodeObject:self.project forKey:@"project"];
[coder encodeObject:self.projectId forKey:@"projectId"];
[coder encodeObject:self.projectName forKey:@"projectName"];
[coder encodeBool:self.selected forKey:@"selected"];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super init];
if (self) {
_planId = [coder decodeObjectOfClass:[NSString class] forKey:@"planId"];
_displayName = [coder decodeObjectOfClass:[NSString class] forKey:@"displayName"];
_project = [coder decodeObjectOfClass:[NSDictionary class] forKey:@"project"];
_projectId = [coder decodeObjectOfClass:[NSString class] forKey:@"projectId"];
_projectName = [coder decodeObjectOfClass:[NSString class] forKey:@"projectName"];
_selected = [coder decodeBoolForKey:@"selected"];
}
return self;
}
@end
@interface SyncProject : NSObject
......@@ -445,15 +472,15 @@ didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
return;
}
NSLog(@"🌐 No cached PROJECT_LIST, fetching from API");
[self fetchProjectList];
NSLog(@"🌐 No cached PROJECT_LIST, requesting from API");
[self requestProjectList];
}
- (void)fetchProjectList {
- (void)requestProjectList {
[self showLoading:YES];
[APIClient fetchProjectList:^(NSDictionary *data, NSError *error) {
[APIClient requestProjectList:^(NSDictionary *data, NSError *error) {
// debug
NSString *plainText = [NSString stringWithFormat:@"%@", data];
......@@ -648,8 +675,11 @@ didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
- (void)onDone {
NSLog(@"Selected units count: %lu",
(unsigned long)self.selectedUnits.count);
[LocalStorage saveArray:self.selectedUnits
forKey:@"SELECTED_OFFLINE_UNITS"];
NSMutableArray *dictArray = [NSMutableArray array];
for (SyncUnit *unit in self.selectedUnits) {
[dictArray addObject:[self dictionaryFromUnit:unit]];
}
[LocalStorage saveArray:dictArray forKey:@"SELECTED_OFFLINE_UNITS"];
[self showOfflineUnit];
}
......
......@@ -5,8 +5,8 @@
#import "SyncHistoryViewController.h"
#import "SyncPendingViewController.h"
#import "SyncProjectViewController.h"
#import "SyncEngine.h"
#import "LocalStorage.h"
#import "OfflineSyncManager.h"
typedef NS_ENUM(NSInteger, SyncCardType) {
SyncCardTypeOfflineUnits = 1,
......@@ -14,6 +14,8 @@ typedef NS_ENUM(NSInteger, SyncCardType) {
SyncCardTypeHistory
};
static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
@interface SyncScreenViewController ()
@property (nonatomic, strong) UIView *headerView;
......@@ -71,7 +73,7 @@ typedef NS_ENUM(NSInteger, SyncCardType) {
self.isSyncing = NO;
//placeholders
self.pendingItemCount = 0;
self.pendingItemCount = 1;
self.offlineUnitCount = 1;
self.historyCount = 1;
......@@ -87,6 +89,7 @@ typedef NS_ENUM(NSInteger, SyncCardType) {
self.pendingItemCount = [self loadPendingItemCount];
self.offlineUnitCount = [self loadOfflineUnitCount];
self.historyCount = [self loadHistoryItemCount];
[self refreshUI];
}
......@@ -126,18 +129,9 @@ typedef NS_ENUM(NSInteger, SyncCardType) {
}
- (void)updateLastUpdatedLabel {
if (!self.lastUpdatedDate) {
self.lastUpdatedLabel.text = @"Last Updated:\n--";
return;
}
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
fmt.dateStyle = NSDateFormatterMediumStyle;
fmt.timeStyle = NSDateFormatterShortStyle;
NSString *dateString = [self lastSyncDisplayString];
self.lastUpdatedLabel.text =
[NSString stringWithFormat:@"Last Updated:\n%@",
[fmt stringFromDate:self.lastUpdatedDate]];
[NSString stringWithFormat:@"Last Updated:\n%@", dateString];
}
#pragma mark - Header
......@@ -500,7 +494,6 @@ typedef NS_ENUM(NSInteger, SyncCardType) {
}
- (void)onOfflineUnitsTapped {
if (self.offlineUnitCount == 0) return;
NSLog(@"Offline Units tapped");
SyncProjectViewController *nextVC = [[SyncProjectViewController alloc] init];
......@@ -570,39 +563,48 @@ typedef NS_ENUM(NSInteger, SyncCardType) {
[self refreshUI];
[self showProgressOverlay];
// SyncEngine *engine = [SyncEngine new];
// engine.submitPending = self.shouldSubmitPending;
// engine.downloadUnits = self.shouldDownloadUnits;
//
// __weak typeof(self) weakSelf = self;
//
// [engine startWithProgress:^(NSInteger step,
// NSInteger total,
// NSString *title) {
//
// weakSelf.progressLabel.text =
// [NSString stringWithFormat:@"%@ (%ld/%ld)",
// title, (long)step, (long)total];
//
// weakSelf.progressBar.progress =
// (float)step / (float)total;
//
// } completion:^(BOOL success, NSError *error) {
//
// [weakSelf.syncProgressOverlay removeFromSuperview];
// weakSelf.syncProgressOverlay = nil;
//
// weakSelf.isSyncing = NO;
//
// if (success) {
// weakSelf.lastUpdatedDate = [NSDate date];
// } else {
// NSLog(@"❌ Sync failed: %@", error.localizedDescription);
// // TODO: show alert
// }
//
// [weakSelf refreshUI];
// }];
__weak typeof(self) weakSelf = self;
// 1️⃣ Manual pending sync
if (self.shouldSubmitPending) {
NSLog(@"Syncing Pending Items");
[[OfflineSyncManager shared]
processNextItemWithProgress:^(NSInteger step, NSInteger total, NSString *title) {
self.progressLabel.text =
[NSString stringWithFormat:@"%ld / %ld", step, total];
self.titleLabel.text = title;
self.progressBar.progress =
total > 0 ? (float)step / (float)total : 0;
} completion:^(BOOL finished) {
self.pendingItemCount = [self loadPendingItemCount];
self.historyCount = [self loadHistoryItemCount];
[self refreshUI];
}];
}
// 2️⃣ Unit + image download sync
if (self.shouldDownloadUnits) {
NSLog(@"Downloading Units");
[[OfflineSyncManager shared]
startDownloadUnitImagesWithProgress:^(NSInteger completed, NSInteger total) {
weakSelf.progressLabel.text =
[NSString stringWithFormat:@"Downloading Units (%ld/%ld)",
(long)completed, (long)total];
weakSelf.progressBar.progress =
total > 0 ? (float)completed / (float)total : 0;
} completion:^(BOOL success) {
[weakSelf finishSyncWithSuccess:success];
}];
}
}
- (void)dismissSyncModal {
......@@ -630,4 +632,38 @@ typedef NS_ENUM(NSInteger, SyncCardType) {
return units.count;
}
- (NSInteger)loadHistoryItemCount {
NSArray *units = [LocalStorage loadArrayForKey:@"SYNC_HISTORY_QUEUE"];
return units.count;
}
- (void)finishSyncWithSuccess:(BOOL)success {
[self.syncProgressOverlay removeFromSuperview];
self.syncProgressOverlay = nil;
self.isSyncing = NO;
if (success) {
self.lastUpdatedDate = [NSDate date];
}
[self refreshUI];
}
- (NSString *)lastSyncDisplayString {
NSNumber *ts = [LocalStorage loadObjectForKey:kLastOfflineSyncDateKey];
if (!ts) {
return @"--";
}
NSDate *date = [NSDate dateWithTimeIntervalSince1970:ts.doubleValue];
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
formatter.dateStyle = NSDateFormatterMediumStyle;
formatter.timeStyle = NSDateFormatterShortStyle;
return [formatter stringFromDate:date];
}
@end
......@@ -6,12 +6,12 @@ NS_ASSUME_NONNULL_BEGIN
@interface APIClient : NSObject
+ (void)fetchAccessItem:(NSString *)unitId
+ (void)requestAccessItem:(NSString *)unitId
completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion;
+ (void)fetchGetUnitPlan:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion;
+ (void)requestGetUnitPlan:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion;
+ (void)fetchSettingsByLocation:(NSString * _Nonnull)locationID
+ (void)requestSettingsByLocation:(NSString * _Nonnull)locationID
projectID:(NSString * _Nonnull)projectID
completion:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion;
......@@ -23,9 +23,9 @@ NS_ASSUME_NONNULL_BEGIN
images:(NSArray<UIImage *> * _Nullable)images
completion:(void (^)(NSDictionary * _Nullable response, NSError * _Nullable error))completion;
+ (void)fetchAppointment:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion;
+ (void)requestAppointment:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion;
+ (void)fetchGeneralInfo:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion;
+ (void)requestGeneralInfo:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion;
+ (void)submitAppointment:(NSDictionary *)appointmentData
completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion;
......@@ -33,15 +33,15 @@ NS_ASSUME_NONNULL_BEGIN
+ (void)submitRepresentativeAppointment:(NSDictionary *)repData
completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion;
+ (void)fetchCancelAppointment:(id)appointmentId
+ (void)requestCancelAppointment:(id)appointmentId
reason:(NSString *)reason
completion:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion;
+ (void)fetchDashboardInfo:(void (^)(NSDictionary *data, NSError *error))completion;
+ (void)requestDashboardInfo:(void (^)(NSDictionary *data, NSError *error))completion;
+ (void)fetchAnnouncement:(void (^)(NSDictionary *data, NSError *error))completion;
+ (void)requestAnnouncement:(void (^)(NSDictionary *data, NSError *error))completion;
+ (void)fetchHistory:(NSString *)issueID
+ (void)requestHistory:(NSString *)issueID
completion:(void (^)(NSDictionary *data, NSError *error))completion;
+(void)submitAddInfo:(NSString *)issueID
......@@ -55,23 +55,29 @@ NS_ASSUME_NONNULL_BEGIN
mediaType:(NSString *)mediaType
completion:(void (^)(NSDictionary *data, NSError *error))completion;
+ (void)fetchIssues:(void (^)(NSDictionary *data, NSError *error))completion;
+ (void)requestIssues:(void (^)(NSDictionary *data, NSError *error))completion;
+ (void)fetchVoidIssue:(id)issueID
+ (void)requestVoidIssue:(id)issueID
remarks:(NSString *)remarks
completion:(void (^)(NSDictionary *data, NSError *error))completion;
+ (void)fetchDeleteIssue:(id)issueID
+ (void)requestDeleteIssue:(id)issueID
remarks:(NSString *)remarks
completion:(void (^)(NSDictionary *data, NSError *error))completion;
+ (void)fetchCommonArea:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion;
+ (void)requestCommonArea:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion;
+ (void)fetchGeneralSettings:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion;
+ (void)requestGeneralSettings:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion;
+ (void)fetchDefectMatrix:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion;
+ (void)requestDefectMatrix:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion;
+ (void)fetchProjectList:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion;
+ (void)requestProjectList:(void (^)(NSDictionary * _Nullable data, NSError * _Nullable error))completion;
+ (void)requestArrayImage:(NSArray *)imageArray
completion:(void (^)(NSDictionary *data, NSError *error))completion;
+ (void)downloadAndCacheImage:(NSURL *)url
completion:(void (^)(BOOL success))completion;
@end
......
......@@ -4,7 +4,7 @@
@implementation APIClient
+ (void)fetchAccessItem:(NSString *)unitId
+ (void)requestAccessItem:(NSString *)unitId
completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion {
NSString *urlString = @"https://kitadev.commudesk.com/api/owner/plan/getAccessItem?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImF0aGlyYWh6YWlkaUBjb252ZXAuY29tIiwicGFzc3dvcmQiOiIkMnkkMTAkNU9xYklqMnZ6UHJwckJrcVd5c3I2LmJCc1hVYS9Hd014eUhnL3RhUUhPUkNQcGdmTG8yakciLCJzdWIiOjIxNzAsImlzcyI6Imh0dHBzOi8va2l0YWRldi5jb21tdWRlc2suY29tL2FwaS9vd25lci9hdXRoL3Bhc3N3b3JkbGVzc19sb2dpbiIsImlhdCI6MTc2MDMxNTM1MiwiZXhwIjoyMDc1ODg0ODcyLCJuYmYiOjE3NjAzMTUzNTIsImp0aSI6IktoQmNyQnJEdDVNdDZlWmMifQ.JnxGbZ7hTeoOr6oibIzZMphgyKtTo2Aq9Efx6mrGfFA";
......@@ -79,7 +79,7 @@
[task resume];
}
+ (void)fetchGetUnitPlan:(void (^)(NSDictionary *data, NSError *error))completion {
+ (void)requestGetUnitPlan:(void (^)(NSDictionary *data, NSError *error))completion {
// ✅ Construct URL (with token)
NSString *urlString = @"https://kitadev.commudesk.com/api/owner/plan/getLocationByUnit?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImF0aGlyYWh6YWlkaUBjb252ZXAuY29tIiwicGFzc3dvcmQiOiIkMnkkMTAkNU9xYklqMnZ6UHJwckJrcVd5c3I2LmJCc1hVYS9Hd014eUhnL3RhUUhPUkNQcGdmTG8yakciLCJzdWIiOjIxNzAsImlzcyI6Imh0dHBzOi8va2l0YWRldi5jb21tdWRlc2suY29tL2FwaS9vd25lci9hdXRoL3Bhc3N3b3JkbGVzc19sb2dpbiIsImlhdCI6MTc2MDMxNTM1MiwiZXhwIjoyMDc1ODg0ODcyLCJuYmYiOjE3NjAzMTUzNTIsImp0aSI6IktoQmNyQnJEdDVNdDZlWmMifQ.JnxGbZ7hTeoOr6oibIzZMphgyKtTo2Aq9Efx6mrGfFA";
......@@ -88,7 +88,7 @@
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";
// 🧱 multipart/form-data setup (same as fetchAnnouncement)
// 🧱 multipart/form-data setup (same as requestAnnouncement)
NSString *boundary = [NSString stringWithFormat:@"Boundary-%@", [[NSUUID UUID] UUIDString]];
NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary];
[request setValue:contentType forHTTPHeaderField:@"Content-Type"];
......@@ -120,8 +120,8 @@
request.HTTPBody = body;
// 🌍 Debug logs
NSLog(@"🌍 [fetchUnitPlan] URL: %@", urlString);
NSLog(@"📦 [fetchUnitPlan] Body:\n%@", [[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding]);
NSLog(@"🌍 [requestUnitPlan] URL: %@", urlString);
NSLog(@"📦 [requestUnitPlan] Body:\n%@", [[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding]);
// 🌐 Perform request
NSURLSessionDataTask *task = [[NSURLSession sharedSession]
......@@ -129,7 +129,7 @@
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
NSLog(@"❌ [fetchUnitPlan] Network error: %@", error);
NSLog(@"❌ [requestUnitPlan] Network error: %@", error);
if (completion) completion(nil, error);
return;
}
......@@ -138,16 +138,16 @@
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonErr];
if (jsonErr) {
NSLog(@"❌ [fetchUnitPlan] JSON parse error: %@", jsonErr);
NSLog(@"❌ [requestUnitPlan] JSON parse error: %@", jsonErr);
if (completion) completion(nil, jsonErr);
return;
}
NSLog(@"✅ [fetchUnitPlan] Response:\n%@", json);
NSLog(@"✅ [requestUnitPlan] Response:\n%@", json);
NSDictionary *appData = json[@"AppData"];
if ([appData[@"status"] isEqualToString:@"success"]) {
NSLog(@"📄 Successfully fetched unit plan for ID");
NSLog(@"📄 Successfully requested unit plan for ID");
} else if (appData[@"error_code"] && [appData[@"error_code"] integerValue] == 503) {
NSLog(@"⚠️ Server under maintenance (503)");
} else {
......@@ -165,7 +165,7 @@
[task resume];
}
+ (void)fetchSettingsByLocation:(NSString *)locationID
+ (void)requestSettingsByLocation:(NSString *)locationID
projectID:(NSString *)projectID
completion:(void (^)(NSDictionary *data, NSError *error))completion {
NSString *urlString = @"https://kitadev.commudesk.com/api/issue/getGeneralSettingByLocation?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImF0aGlyYWh6YWlkaUBjb252ZXAuY29tIiwicGFzc3dvcmQiOiIkMnkkMTAkNU9xYklqMnZ6UHJwckJrcVd5c3I2LmJCc1hVYS9Hd014eUhnL3RhUUhPUkNQcGdmTG8yakciLCJzdWIiOjIxNzAsImlzcyI6Imh0dHBzOi8va2l0YWRldi5jb21tdWRlc2suY29tL2FwaS9vd25lci9hdXRoL3Bhc3N3b3JkbGVzc19sb2dpbiIsImlhdCI6MTc2MDMxNTM1MiwiZXhwIjoyMDc1ODg0ODcyLCJuYmYiOjE3NjAzMTUzNTIsImp0aSI6IktoQmNyQnJEdDVNdDZlWmMifQ.JnxGbZ7hTeoOr6oibIzZMphgyKtTo2Aq9Efx6mrGfFA";
......@@ -206,8 +206,8 @@
request.HTTPBody = body;
// 🌍 Debug logs
NSLog(@"🌍 [fetchSettingsByLocation] URL: %@", urlString);
NSLog(@"📦 [fetchSettingsByLocation] Body:\n%@", [[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding]);
NSLog(@"🌍 [requestSettingsByLocation] URL: %@", urlString);
NSLog(@"📦 [requestSettingsByLocation] Body:\n%@", [[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding]);
// 🌐 Send request
NSURLSessionDataTask *task = [[NSURLSession sharedSession]
......@@ -215,7 +215,7 @@
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
NSLog(@"❌ [fetchSettingsByLocation] Network error: %@", error);
NSLog(@"❌ [requestSettingsByLocation] Network error: %@", error);
if (completion) completion(nil, error);
return;
}
......@@ -228,13 +228,13 @@
return;
}
NSLog(@"✅ [fetchSettingsByLocation] Response:\n%@", json);
NSLog(@"✅ [requestSettingsByLocation] Response:\n%@", json);
NSDictionary *appData = json[@"AppData"];
NSString *status = appData[@"status"];
if ([status isEqualToString:@"success"]) {
NSLog(@"📄 Successfully fetched settings for location");
NSLog(@"📄 Successfully requested settings for location");
}
else if ([appData[@"error_code"] integerValue] == 503) {
NSLog(@"⚠️ Maintenance mode (503)");
......@@ -451,7 +451,7 @@
[task resume];
}
+ (void)fetchAppointment:(void (^)(NSDictionary *data, NSError *error))completion {
+ (void)requestAppointment:(void (^)(NSDictionary *data, NSError *error))completion {
// ✅ URL
NSString *urlString = @"https://kitadev.commudesk.com/api/owner/appointment/getSlot?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImF0aGlyYWh6YWlkaUBjb252ZXAuY29tIiwicGFzc3dvcmQiOiIkMnkkMTAkNU9xYklqMnZ6UHJwckJrcVd5c3I2LmJCc1hVYS9Hd014eUhnL3RhUUhPUkNQcGdmTG8yakciLCJzdWIiOjIxNzAsImlzcyI6Imh0dHBzOi8va2l0YWRldi5jb21tdWRlc2suY29tL2FwaS9vd25lci9hdXRoL3Bhc3N3b3JkbGVzc19sb2dpbiIsImlhdCI6MTc2MDMxNTM1MiwiZXhwIjoyMDc1ODg0ODcyLCJuYmYiOjE3NjAzMTUzNTIsImp0aSI6IktoQmNyQnJEdDVNdDZlWmMifQ.JnxGbZ7hTeoOr6oibIzZMphgyKtTo2Aq9Efx6mrGfFA";
NSURL *url = [NSURL URLWithString:urlString];
......@@ -516,7 +516,7 @@
[task resume];
}
+ (void)fetchGeneralInfo:(void (^)(NSDictionary *data, NSError *error))completion {
+ (void)requestGeneralInfo:(void (^)(NSDictionary *data, NSError *error))completion {
// ✅ URL
NSString *urlString = @"https://kitadev.commudesk.com/api/owner/plan/getUnitInfo?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImF0aGlyYWh6YWlkaUBjb252ZXAuY29tIiwicGFzc3dvcmQiOiIkMnkkMTAkNU9xYklqMnZ6UHJwckJrcVd5c3I2LmJCc1hVYS9Hd014eUhnL3RhUUhPUkNQcGdmTG8yakciLCJzdWIiOjIxNzAsImlzcyI6Imh0dHBzOi8va2l0YWRldi5jb21tdWRlc2suY29tL2FwaS9vd25lci9hdXRoL3Bhc3N3b3JkbGVzc19sb2dpbiIsImlhdCI6MTc2MDMxNTM1MiwiZXhwIjoyMDc1ODg0ODcyLCJuYmYiOjE3NjAzMTUzNTIsImp0aSI6IktoQmNyQnJEdDVNdDZlWmMifQ.JnxGbZ7hTeoOr6oibIzZMphgyKtTo2Aq9Efx6mrGfFA";
NSURL *url = [NSURL URLWithString:urlString];
......@@ -707,7 +707,7 @@
[task resume];
}
+ (void)fetchCancelAppointment:(id)appointmentId
+ (void)requestCancelAppointment:(id)appointmentId
reason:(NSString *)reason
completion:(void (^)(NSDictionary *data, NSError *error))completion {
// ✅ URL
......@@ -763,7 +763,7 @@
if (jsonParseError) {
NSLog(@"⚠️ JSON Parse Error: %@", jsonParseError);
} else {
NSLog(@"✅ Fetch Cancel Appointment Response:\n%@", jsonResponse);
NSLog(@"✅ request Cancel Appointment Response:\n%@", jsonResponse);
}
dispatch_async(dispatch_get_main_queue(), ^{
......@@ -774,7 +774,7 @@
[task resume];
}
+ (void)fetchDashboardInfo:(void (^)(NSDictionary *data, NSError *error))completion {
+ (void)requestDashboardInfo:(void (^)(NSDictionary *data, NSError *error))completion {
NSString *urlString = @"https://kitadev.commudesk.com/api/owner/plan/getIssueUpdateAppointmentInfo";
NSURL *url = [NSURL URLWithString:urlString];
......@@ -837,7 +837,7 @@
[task resume];
}
+ (void)fetchAnnouncement:(void (^)(NSDictionary *data, NSError *error))completion {
+ (void)requestAnnouncement:(void (^)(NSDictionary *data, NSError *error))completion {
// ✅ Keep token in URL
NSString *urlString = @"https://kitadev.commudesk.com/api/clientAnnouncement?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImF0aGlyYWh6YWlkaUBjb252ZXAuY29tIiwicGFzc3dvcmQiOiIkMnkkMTAkNU9xYklqMnZ6UHJwckJrcVd5c3I2LmJCc1hVYS9Hd014eUhnL3RhUUhPUkNQcGdmTG8yakciLCJzdWIiOjIxNzAsImlzcyI6Imh0dHBzOi8va2l0YWRldi5jb21tdWRlc2suY29tL2FwaS9vd25lci9hdXRoL3Bhc3N3b3JkbGVzc19sb2dpbiIsImlhdCI6MTc2MDMxNTM1MiwiZXhwIjoyMDc1ODg0ODcyLCJuYmYiOjE3NjAzMTUzNTIsImp0aSI6IktoQmNyQnJEdDVNdDZlWmMifQ.JnxGbZ7hTeoOr6oibIzZMphgyKtTo2Aq9Efx6mrGfFA";
NSURL *url = [NSURL URLWithString:urlString];
......@@ -894,7 +894,7 @@
announcements = json[@"Data"];
NSLog(@"📣 Got %lu announcements", (unsigned long)announcements.count);
} else {
NSLog(@"⚠️ Announcement fetch failed: %@", appData[@"message"]);
NSLog(@"⚠️ Announcement request failed: %@", appData[@"message"]);
}
if (completion) {
......@@ -906,7 +906,7 @@
[task resume];
}
+ (void)fetchHistory:(NSString *)issueID
+ (void)requestHistory:(NSString *)issueID
completion:(void (^)(NSDictionary *data, NSError *error))completion {
// ✅ URL
NSString *urlString = @"https://kitadev.commudesk.com/api/owner/issue/getIssueHistory?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImF0aGlyYWh6YWlkaUBjb252ZXAuY29tIiwicGFzc3dvcmQiOiIkMnkkMTAkNU9xYklqMnZ6UHJwckJrcVd5c3I2LmJCc1hVYS9Hd014eUhnL3RhUUhPUkNQcGdmTG8yakciLCJzdWIiOjIxNzAsImlzcyI6Imh0dHBzOi8va2l0YWRldi5jb21tdWRlc2suY29tL2FwaS9vd25lci9hdXRoL3Bhc3N3b3JkbGVzc19sb2dpbiIsImlhdCI6MTc2MDMxNTM1MiwiZXhwIjoyMDc1ODg0ODcyLCJuYmYiOjE3NjAzMTUzNTIsImp0aSI6IktoQmNyQnJEdDVNdDZlWmMifQ.JnxGbZ7hTeoOr6oibIzZMphgyKtTo2Aq9Efx6mrGfFA";
......@@ -1175,7 +1175,7 @@
[task resume];
}
+ (void)fetchIssues:(void (^)(NSDictionary *data, NSError *error))completion {
+ (void)requestIssues:(void (^)(NSDictionary *data, NSError *error))completion {
// ✅ URL
NSString *urlString = @"https://kitadev.commudesk.com/api/owner/issue/getIssue?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImF0aGlyYWh6YWlkaUBjb252ZXAuY29tIiwicGFzc3dvcmQiOiIkMnkkMTAkNU9xYklqMnZ6UHJwckJrcVd5c3I2LmJCc1hVYS9Hd014eUhnL3RhUUhPUkNQcGdmTG8yakciLCJzdWIiOjIxNzAsImlzcyI6Imh0dHBzOi8va2l0YWRldi5jb21tdWRlc2suY29tL2FwaS9vd25lci9hdXRoL3Bhc3N3b3JkbGVzc19sb2dpbiIsImlhdCI6MTc2MDMxNTM1MiwiZXhwIjoyMDc1ODg0ODcyLCJuYmYiOjE3NjAzMTUzNTIsImp0aSI6IktoQmNyQnJEdDVNdDZlWmMifQ.JnxGbZ7hTeoOr6oibIzZMphgyKtTo2Aq9Efx6mrGfFA";
NSURL *url = [NSURL URLWithString:urlString];
......@@ -1248,7 +1248,7 @@
[task resume];
}
+ (void)fetchVoidIssue:(id)issueID
+ (void)requestVoidIssue:(id)issueID
remarks:(NSString *)remarks
completion:(void (^)(NSDictionary *data, NSError *error))completion {
// ✅ URL
......@@ -1322,7 +1322,7 @@
[task resume];
}
+ (void)fetchDeleteIssue:(NSString *)issueID
+ (void)requestDeleteIssue:(NSString *)issueID
remarks:(NSString *)remarks
completion:(void (^)(NSDictionary *data, NSError *error))completion {
// ✅ URL
......@@ -1389,7 +1389,7 @@
[task resume];
}
+ (void)fetchCommonArea:(void (^)(NSDictionary *data, NSError *error))completion {
+ (void)requestCommonArea:(void (^)(NSDictionary *data, NSError *error))completion {
// ✅ URL
NSString *urlString = @"https://kitadev.commudesk.com/api/owner/plan/getCommonArea?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImF0aGlyYWh6YWlkaUBjb252ZXAuY29tIiwicGFzc3dvcmQiOiIkMnkkMTAkNU9xYklqMnZ6UHJwckJrcVd5c3I2LmJCc1hVYS9Hd014eUhnL3RhUUhPUkNQcGdmTG8yakciLCJzdWIiOjIxNzAsImlzcyI6Imh0dHBzOi8va2l0YWRldi5jb21tdWRlc2suY29tL2FwaS9vd25lci9hdXRoL3Bhc3N3b3JkbGVzc19sb2dpbiIsImlhdCI6MTc2MDMxNTM1MiwiZXhwIjoyMDc1ODg0ODcyLCJuYmYiOjE3NjAzMTUzNTIsImp0aSI6IktoQmNyQnJEdDVNdDZlWmMifQ.JnxGbZ7hTeoOr6oibIzZMphgyKtTo2Aq9Efx6mrGfFA";
NSURL *url = [NSURL URLWithString:urlString];
......@@ -1463,9 +1463,9 @@
[task resume];
}
+ (void)fetchGeneralSettings:(void (^)(NSDictionary *data, NSError *error))completion {
+ (void)requestGeneralSettings:(void (^)(NSDictionary *data, NSError *error))completion {
// ✅ URL
NSString *urlString = @"https://kitadev.commudesk.com/api/owner/plan/getCommonArea?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImF0aGlyYWh6YWlkaUBjb252ZXAuY29tIiwicGFzc3dvcmQiOiIkMnkkMTAkNU9xYklqMnZ6UHJwckJrcVd5c3I2LmJCc1hVYS9Hd014eUhnL3RhUUhPUkNQcGdmTG8yakciLCJzdWIiOjIxNzAsImlzcyI6Imh0dHBzOi8va2l0YWRldi5jb21tdWRlc2suY29tL2FwaS9vd25lci9hdXRoL3Bhc3N3b3JkbGVzc19sb2dpbiIsImlhdCI6MTc2MDMxNTM1MiwiZXhwIjoyMDc1ODg0ODcyLCJuYmYiOjE3NjAzMTUzNTIsImp0aSI6IktoQmNyQnJEdDVNdDZlWmMifQ.JnxGbZ7hTeoOr6oibIzZMphgyKtTo2Aq9Efx6mrGfFA";
NSString *urlString = @"https://kitadev.commudesk.com/api/issue/getGeneralSetting?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImF0aGlyYWh6YWlkaUBjb252ZXAuY29tIiwicGFzc3dvcmQiOiIkMnkkMTAkNU9xYklqMnZ6UHJwckJrcVd5c3I2LmJCc1hVYS9Hd014eUhnL3RhUUhPUkNQcGdmTG8yakciLCJzdWIiOjIxNzAsImlzcyI6Imh0dHBzOi8va2l0YWRldi5jb21tdWRlc2suY29tL2FwaS9vd25lci9hdXRoL3Bhc3N3b3JkbGVzc19sb2dpbiIsImlhdCI6MTc2MDMxNTM1MiwiZXhwIjoyMDc1ODg0ODcyLCJuYmYiOjE3NjAzMTUzNTIsImp0aSI6IktoQmNyQnJEdDVNdDZlWmMifQ.JnxGbZ7hTeoOr6oibIzZMphgyKtTo2Aq9Efx6mrGfFA";
NSURL *url = [NSURL URLWithString:urlString];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
......@@ -1474,12 +1474,22 @@
// ✅ JSON headers
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
// ✅ Device info dictionary
NSDictionary *deviceInfo = @{
@"OS": @"AND",
@"IMEI": @"AND:2a2f13ff17b1cbb5",
@"OS_VERSION": @"35",
@"MODEL": @"2306EPN60G",
@"APP_VERSION": @"1.22.26"
};
// ✅ Build JSON body
NSDictionary *jsonBody = @{
@"data": @{
@"os": @"AND",
@"project_id": @"8",
@"information": deviceInfo,
}
};
......@@ -1527,7 +1537,7 @@
[task resume];
}
+ (void)fetchDefectMatrix:(void (^)(NSDictionary *data, NSError *error))completion {
+ (void)requestDefectMatrix:(void (^)(NSDictionary *data, NSError *error))completion {
// ✅ URL
NSString *urlString = @"https://kitadev.commudesk.com/api/issue/getGeneralSettingByUnit?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImF0aGlyYWh6YWlkaUBjb252ZXAuY29tIiwicGFzc3dvcmQiOiIkMnkkMTAkNU9xYklqMnZ6UHJwckJrcVd5c3I2LmJCc1hVYS9Hd014eUhnL3RhUUhPUkNQcGdmTG8yakciLCJzdWIiOjIxNzAsImlzcyI6Imh0dHBzOi8va2l0YWRldi5jb21tdWRlc2suY29tL2FwaS9vd25lci9hdXRoL3Bhc3N3b3JkbGVzc19sb2dpbiIsImlhdCI6MTc2MDMxNTM1MiwiZXhwIjoyMDc1ODg0ODcyLCJuYmYiOjE3NjAzMTUzNTIsImp0aSI6IktoQmNyQnJEdDVNdDZlWmMifQ.JnxGbZ7hTeoOr6oibIzZMphgyKtTo2Aq9Efx6mrGfFA";
NSURL *url = [NSURL URLWithString:urlString];
......@@ -1602,7 +1612,7 @@
[task resume];
}
+ (void)fetchProjectList:(void (^)(NSDictionary *data, NSError *error))completion {
+ (void)requestProjectList:(void (^)(NSDictionary *data, NSError *error))completion {
// ✅ URL
NSString *urlString = @"https://kitadev.commudesk.com/api/owner/project/listProject?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImF0aGlyYWh6YWlkaUBjb252ZXAuY29tIiwicGFzc3dvcmQiOiIkMnkkMTAkNU9xYklqMnZ6UHJwckJrcVd5c3I2LmJCc1hVYS9Hd014eUhnL3RhUUhPUkNQcGdmTG8yakciLCJzdWIiOjIxNzAsImlzcyI6Imh0dHBzOi8va2l0YWRldi5jb21tdWRlc2suY29tL2FwaS9vd25lci9hdXRoL3Bhc3N3b3JkbGVzc19sb2dpbiIsImlhdCI6MTc2MDMxNTM1MiwiZXhwIjoyMDc1ODg0ODcyLCJuYmYiOjE3NjAzMTUzNTIsImp0aSI6IktoQmNyQnJEdDVNdDZlWmMifQ.JnxGbZ7hTeoOr6oibIzZMphgyKtTo2Aq9Efx6mrGfFA";
NSURL *url = [NSURL URLWithString:urlString];
......@@ -1665,6 +1675,140 @@
[task resume];
}
+ (void)requestArrayImage:(NSArray *)imageArray
completion:(void (^)(NSDictionary *data, NSError *error))completion {
// ✅ URL
NSString *urlString = @"https://kitadev.commudesk.com/api/owner/offline/syncImageArray?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImF0aGlyYWh6YWlkaUBjb252ZXAuY29tIiwicGFzc3dvcmQiOiIkMnkkMTAkNU9xYklqMnZ6UHJwckJrcVd5c3I2LmJCc1hVYS9Hd014eUhnL3RhUUhPUkNQcGdmTG8yakciLCJzdWIiOjIxNzAsImlzcyI6Imh0dHBzOi8va2l0YWRldi5jb21tdWRlc2suY29tL2FwaS9vd25lci9hdXRoL3Bhc3N3b3JkbGVzc19sb2dpbiIsImlhdCI6MTc2MDMxNTM1MiwiZXhwIjoyMDc1ODg0ODcyLCJuYmYiOjE3NjAzMTUzNTIsImp0aSI6IktoQmNyQnJEdDVNdDZlWmMifQ.JnxGbZ7hTeoOr6oibIzZMphgyKtTo2Aq9Efx6mrGfFA";
NSURL *url = [NSURL URLWithString:urlString];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";
// ✅ JSON headers
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
// ✅ Build JSON body
NSDictionary *jsonBody = @{
@"data": imageArray,
};
// ✅ Convert dictionary → NSData
NSError *jsonError;
NSData *bodyData = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:&jsonError];
if (jsonError) {
NSLog(@"❌ JSON Serialization Error: %@", jsonError);
if (completion) completion(nil, jsonError);
return;
}
request.HTTPBody = bodyData;
// 🧭 Debug logs
NSLog(@"🌍 URL: %@", urlString);
NSLog(@"📦 Body JSON: %@", jsonBody);
// ✅ Make request
NSURLSessionDataTask *task = [[NSURLSession sharedSession]
dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
NSLog(@"❌ Network Error: %@", error);
dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(nil, error);
});
return;
}
NSError *jsonParseError;
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonParseError];
if (jsonParseError) {
NSLog(@"⚠️ JSON Parse Error: %@", jsonParseError);
} else {
NSLog(@"✅ Project List API Response:\n%@", jsonResponse);
}
dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(jsonResponse, jsonParseError);
});
}];
[task resume];
}
// + (void)downloadAndCacheImage:(NSURL *)url
// completion:(void (^)(BOOL success))completion {
// if (!url) {
// if (completion) completion(NO);
// return;
// }
// // 1️⃣ Build cache path
// NSString *fileName = url.lastPathComponent;
// // NSString *cacheDir = [self offlineImageCacheDirectory];
// NSString *filePath = [cacheDir stringByAppendingPathComponent:fileName];
// // 2️⃣ Skip if already cached
// if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
// NSLog(@"🟢 Cached file exists: %@", fileName);
// if (completion) completion(YES);
// return;
// }
// // 3️⃣ Build request with timeout
// NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// request.HTTPMethod = @"GET";
// request.timeoutInterval = 20; // ⏱️ per-file timeout
// NSURLSessionConfiguration *config =
// [NSURLSessionConfiguration defaultSessionConfiguration];
// config.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
// NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
// // 4️⃣ Download task
// NSURLSessionDownloadTask *task =
// [session downloadTaskWithRequest:request
// completionHandler:^(NSURL *location,
// NSURLResponse *response,
// NSError *error) {
// if (error || !location) {
// NSLog(@"❌ Download failed: %@ | %@", url, error);
// dispatch_async(dispatch_get_main_queue(), ^{
// if (completion) completion(NO);
// });
// return;
// }
// // 5️⃣ Move file to cache
// NSError *moveError;
// [[NSFileManager defaultManager]
// moveItemAtURL:location
// toURL:[NSURL fileURLWithPath:filePath]
// error:&moveError];
// if (moveError) {
// NSLog(@"❌ Save failed: %@", moveError);
// dispatch_async(dispatch_get_main_queue(), ^{
// if (completion) completion(NO);
// });
// return;
// }
// NSLog(@"✅ Cached: %@", fileName);
// dispatch_async(dispatch_get_main_queue(), ^{
// if (completion) completion(YES);
// });
// }];
// [task resume];
// }
#pragma mark - Helper Methods
+ (NSString *)multipartFormField:(NSString *)name value:(NSString *)value boundary:(NSString *)boundary {
return [NSString stringWithFormat:@"--%@\r\nContent-Disposition: form-data; name=\"data[%@]\"\r\n\r\n%@\r\n", boundary, name, value];
......
......@@ -18,4 +18,8 @@
/// Save array safely (for convenience)
+ (void)saveArray:(NSArray *)array forKey:(NSString *)key;
+ (NSDictionary *)loadDictionaryForKey:(NSString *)key;
+ (void)saveDictionary:(NSDictionary *)dict forKey:(NSString *)key;
@end
......@@ -57,4 +57,16 @@
[self saveObject:array forKey:key];
}
+ (NSDictionary *)loadDictionaryForKey:(NSString *)key {
id object = [self loadObjectForKey:key];
if ([object isKindOfClass:[NSDictionary class]]) {
return object;
}
return nil;
}
+ (void)saveDictionary:(NSDictionary *)dict forKey:(NSString *)key {
[self saveObject:dict forKey:key];
}
@end
......@@ -28,9 +28,22 @@ NS_ASSUME_NONNULL_BEGIN
- (BOOL)hasPendingItems;
- (NSArray *)pendingItemsForUI;
- (NSArray *)historyItemsForUI;
- (void)clearOfflineQueue;
- (void)processNextItemWithProgress:(void (^)(NSInteger step,
NSInteger total,
NSString *title))progress
completion:(void (^)(BOOL finished))completion;
// Retry failed history item
- (void)retryHistoryItem:(NSDictionary *)historyItem;
// Download Units
- (void)startDownloadUnitImagesWithProgress:(void (^)(NSInteger completed, NSInteger total))progress
completion:(void (^)(BOOL success))completion;
@end
NS_ASSUME_NONNULL_END
......@@ -8,6 +8,10 @@ static NSString * const kOfflineQueueKey = @"OFFLINE_SYNC_QUEUE";
static NSString * const kSelectedOfflineUnitsKey = @"SELECTED_OFFLINE_UNITS";
static NSString * const kSyncHistoryKey = @"SYNC_HISTORY_QUEUE";
static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
@implementation OfflineSyncManager
#pragma mark - Singleton
......@@ -22,7 +26,6 @@ static NSString * const kSelectedOfflineUnitsKey = @"SELECTED_OFFLINE_UNITS";
}
#pragma mark - Queue Helpers (CRITICAL)
- (NSMutableArray *)safeLoadQueue {
id stored = [LocalStorage loadArrayForKey:kOfflineQueueKey];
......@@ -113,7 +116,6 @@ static NSString * const kSelectedOfflineUnitsKey = @"SELECTED_OFFLINE_UNITS";
}
#pragma mark - Core Queue Add
- (void)enqueueAction:(NSString *)type
payload:(NSDictionary *)payload
images:(NSArray<UIImage *> *)images
......@@ -144,42 +146,52 @@ static NSString * const kSelectedOfflineUnitsKey = @"SELECTED_OFFLINE_UNITS";
#pragma mark - Processing Queue
- (void)processNextItemWithProgress:(void (^)(NSInteger step,
NSInteger total,
NSString *title))progress
completion:(void (^)(BOOL finished))completion {
- (void)startManualSync {
NSMutableArray *queue = [self safeLoadQueue];
NSInteger total = queue.count;
if (![NetworkManager.sharedManager isConnected]) {
NSLog(@"🚫 [OfflineSync] No internet — aborting manual sync");
if (total == 0) {
if (completion) completion(YES);
return;
}
[self processNextItem];
}
__block NSInteger step = 0;
- (void)processNextItem {
__weak typeof(self) weakSelf = self;
NSMutableArray *queue = [self safeLoadQueue];
if (queue.count == 0) {
NSLog(@"✅ [OfflineSync] Queue empty — sync complete");
return;
}
void (^processNext)(void);
processNext = ^{
NSMutableArray *currentQueue = [weakSelf safeLoadQueue];
NSDictionary *item = queue.firstObject;
if (![item isKindOfClass:[NSDictionary class]]) {
NSLog(@"⚠️ [OfflineSync] Invalid item — removing");
[queue removeObjectAtIndex:0];
[self saveQueue:queue];
[self processNextItem];
return;
}
if (currentQueue.count == 0) {
if (completion) completion(YES);
return;
}
NSDictionary *item = currentQueue.firstObject;
step++;
if (progress) {
NSString *title = item[@"issueDisplay"] ?: item[@"type"] ?: @"Syncing";
progress(step, total, title);
}
NSLog(@"🔄 [OfflineSync] Processing %@", item[@"type"]);
[weakSelf processItem:item completion:^(BOOL success) {
[self processItem:item completion:^(BOOL success) {
// 🔥 Save to history
NSMutableDictionary *historyItem = [item mutableCopy];
historyItem[@"syncedAt"] = [weakSelf todayDateString];
historyItem[@"status"] = success ? @"SUCCESS" : @"FAILED";
if (success) {
NSLog(@"✅ [OfflineSync] Item synced");
NSMutableArray *history = [weakSelf safeLoadHistory];
[history insertObject:historyItem atIndex:0];
[weakSelf saveHistoryQueue:history];
// 🔥 cleanup files
// 🔥 Cleanup files
for (NSString *path in item[@"imagePaths"]) {
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
}
......@@ -187,60 +199,100 @@ static NSString * const kSelectedOfflineUnitsKey = @"SELECTED_OFFLINE_UNITS";
[[NSFileManager defaultManager] removeItemAtPath:item[@"mediaPath"] error:nil];
}
// remove item and continue
NSMutableArray *updatedQueue = [self safeLoadQueue];
// 🔥 Remove processed item
NSMutableArray *updatedQueue = [weakSelf safeLoadQueue];
if (updatedQueue.count > 0) {
[updatedQueue removeObjectAtIndex:0];
[self saveQueue:updatedQueue];
[weakSelf saveQueue:updatedQueue];
}
[self processNextItem]; // move to next
} else {
NSLog(@"❌ [OfflineSync] Sync failed — stopping");
// ❗ DO NOT remove item
// user must tap Sync again
}
}];
// 🔁 Continue
dispatch_async(dispatch_get_main_queue(), ^{
processNext();
});
}];
};
processNext();
}
- (void)processItem:(NSDictionary *)item
completion:(void (^)(BOOL success))completion {
NSString *type = item[@"type"] ?: @"";
// Load files
NSArray<UIImage *> *images = [self loadImagesFromPaths:item[@"imagePaths"]];
NSURL *mediaURL = item[@"mediaPath"] ? [NSURL fileURLWithPath:item[@"mediaPath"]] : nil;
// Define per-item timeout
NSTimeInterval timeout = 60.0; // seconds
__block BOOL completed = NO;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (!completed) {
NSLog(@"⚠️ [OfflineSync] Item timed out: %@, marking as FAILED", item[@"type"]);
completed = YES;
completion(NO);
}
});
void (^finalize)(BOOL) = ^(BOOL success){
if (!completed) {
completed = YES;
completion(success);
}
};
if ([type isEqualToString:@"ADD_INFO"]) {
NSArray *images = [self loadImagesFromPaths:item[@"imagePaths"]];
NSURL *mediaURL = item[@"mediaPath"] ? [NSURL fileURLWithPath:item[@"mediaPath"]] : nil;
BOOL hasImages = images.count > 0;
BOOL hasMedia = mediaURL != nil;
if (!hasImages && !hasMedia) {
if (images.count == 0 && !mediaURL) {
// Simple ADD_INFO without attachments
[APIClient submitAddInfo:item[@"issue_id"]
remarks:item[@"remarks"]
completion:^(NSDictionary *r, NSError *e) {
completion(e == nil);
completion:^(NSDictionary *res, NSError *err) {
finalize(err == nil);
}];
} else {
// ADD_INFO with images/media
[APIClient uploadAddInfo:item[@"issue_id"]
remarks:item[@"remarks"]
images:images
mediaURL:mediaURL
mediaType:item[@"mediaType"]
completion:^(NSDictionary *r, NSError *e) {
completion(e == nil);
completion:^(NSDictionary *res, NSError *err) {
finalize(err == nil);
}];
}
return;
}
NSLog(@"⚠️ Unknown offline type: %@", type);
completion(NO);
else if ([type isEqualToString:@"ADD_ISSUE"]) {
[APIClient submitAddIssue:item[@"payload"]
images:images
completion:^(NSDictionary *res, NSError *err){
finalize(err == nil);
}];
return;
}
else if ([type isEqualToString:@"UPDATE_ISSUE"]) {
[APIClient submitUpdateIssue:item[@"payload"]
images:images
completion:^(NSDictionary *res, NSError *err){
finalize(err == nil);
}];
return;
}
else {
NSLog(@"⚠️ [OfflineSync] Unknown offline type: %@", type);
finalize(NO);
return;
}
}
#pragma mark - Disk Handling
- (NSArray *)saveImagesToDisk:(NSArray<UIImage *> *)images {
NSMutableArray *paths = [NSMutableArray array];
......@@ -342,6 +394,88 @@ static NSString * const kSelectedOfflineUnitsKey = @"SELECTED_OFFLINE_UNITS";
return sections;
}
#pragma mark - History Helpers
- (NSMutableArray *)safeLoadHistory {
id stored = [LocalStorage loadArrayForKey:kSyncHistoryKey];
if (![stored isKindOfClass:[NSArray class]]) {
[LocalStorage removeObjectForKey:kSyncHistoryKey];
return [NSMutableArray array];
}
NSMutableArray *valid = [NSMutableArray array];
for (id obj in stored) {
if ([obj isKindOfClass:[NSDictionary class]]) {
[valid addObject:obj];
}
}
return valid;
}
- (void)saveHistory:(NSArray *)history {
[LocalStorage saveArray:history forKey:kSyncHistoryKey];
}
- (NSArray *)historyItemsForUI {
NSArray *history = [self loadHistoryQueue];
NSMutableArray *sections = [NSMutableArray array];
for (NSDictionary *item in history) {
NSDictionary *uiItem = @{
@"unitName": item[@"unitName"] ?: @"-",
@"createdBy": item[@"createdBy"] ?: @"-",
@"createdOn": item[@"createdAt"] ?: @"-",
@"uploadedOn": item[@"syncedAt"] ?: @"-",
@"type": item[@"issueDisplay"] ?: @"-",
@"status": item[@"status"] ?: @"FAILED",
@"raw": item
};
[sections addObject:uiItem];
}
return sections;
}
- (NSInteger)historyItemCount {
return [self safeLoadHistory].count;
}
- (NSMutableArray *)loadHistoryQueue {
id stored = [LocalStorage loadArrayForKey:kSyncHistoryKey];
if (![stored isKindOfClass:[NSArray class]]) {
return [NSMutableArray array];
}
NSMutableArray *valid = [NSMutableArray array];
for (id obj in stored) {
if ([obj isKindOfClass:[NSDictionary class]]) {
[valid addObject:obj];
}
}
return valid;
}
- (void)saveHistoryQueue:(NSArray *)queue {
[LocalStorage saveArray:queue forKey:kSyncHistoryKey];
}
- (void)retryHistoryItem:(NSDictionary *)historyItem {
NSMutableArray *queue = [self safeLoadQueue];
[queue addObject:historyItem];
[self saveQueue:queue];
NSMutableArray *history = [self loadHistoryQueue];
[history removeObject:historyItem];
[self saveHistoryQueue:history];
}
- (NSString *)formatTimestamp:(NSNumber *)ts {
NSDate *date = [NSDate dateWithTimeIntervalSince1970:ts.doubleValue];
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
......@@ -361,4 +495,283 @@ static NSString * const kSelectedOfflineUnitsKey = @"SELECTED_OFFLINE_UNITS";
NSLog(@"🧹 [OfflineSync] Offline queue cleared");
}
#pragma mark - Download Units
- (void)startDownloadUnitImagesWithProgress:(void (^)(NSInteger completed, NSInteger total))progress
completion:(void (^)(BOOL success))completion {
if (![NetworkManager.sharedManager isConnected]) {
if (completion) completion(NO);
return;
}
NSString *projectId = @"8";
NSString *drawingPlanId = @"135";
NSArray *selectedUnits = [LocalStorage loadArrayForKey:kSelectedOfflineUnitsKey];
if (!selectedUnits) selectedUnits = @[];
// 1️⃣ Build the sequential task queue
NSMutableArray<void (^)(void (^next)(void))> *tasks = [NSMutableArray array];
// 2️⃣ Add 6 independent API calls as tasks
[tasks addObject:^(void (^next)(void)) {
NSString *key = [NSString stringWithFormat:@"ISSUES_%@", drawingPlanId];
[APIClient requestIssues:^(NSDictionary *res, NSError *err) {
if (res) [LocalStorage saveDictionary:res[@"Data"] forKey:key];
next();
}];
}];
[tasks addObject:^(void (^next)(void)) {
NSString *key = [NSString stringWithFormat:@"DASHBOARD_INFO_%@", drawingPlanId];
[APIClient requestDashboardInfo:^(NSDictionary *res, NSError *err) {
if (res) [LocalStorage saveDictionary:res[@"Data"] forKey:key];
next();
}];
}];
[tasks addObject:^(void (^next)(void)) {
NSString *key = [NSString stringWithFormat:@"PLAN_UNIT_%@", drawingPlanId];
[APIClient requestGetUnitPlan:^(NSDictionary *res, NSError *err) {
if (res) {
id dataArray = res[@"Data"];
NSDictionary *objData = @{};
if ([dataArray isKindOfClass:[NSArray class]] && [dataArray count] > 0) {
id firstItem = dataArray[0];
if ([firstItem isKindOfClass:[NSDictionary class]]) {
objData = firstItem;
}
}
[LocalStorage saveDictionary:objData forKey:key];
}
next();
}];
}];
[tasks addObject:^(void (^next)(void)) {
NSString *key = [NSString stringWithFormat:@"SETTINGS_%@", projectId];
[APIClient requestGeneralSettings:^(NSDictionary *res, NSError *err) {
if (res) [LocalStorage saveDictionary:res[@"Data"] forKey:key];
next();
}];
}];
[tasks addObject:^(void (^next)(void)) {
NSString *key = [NSString stringWithFormat:@"COMMONAREA_%@", projectId];
[APIClient requestCommonArea:^(NSDictionary *res, NSError *err) {
if (res) [LocalStorage saveDictionary:res[@"Data"] forKey:key];
next();
}];
}];
[tasks addObject:^(void (^next)(void)) {
[APIClient requestDefectMatrix:^(NSDictionary *res, NSError *err) {
if (!res || err) {
next();
return;
}
NSDictionary *data = res[@"Data"];
if (![data isKindOfClass:[NSDictionary class]]) {
next();
return;
}
// 1️⃣ Shared lists
NSArray *priority = data[@"priority"] ?: @[];
NSArray *contractor = data[@"contractor"] ?: @[];
NSArray *newIssueStatus = data[@"new_issue_status"] ?: @[];
NSArray *startWorkStatus = data[@"start_work_status"] ?: @[];
NSArray *subContractor = data[@"sub_contractor"] ?: @[];
NSArray *locationStatus = data[@"location_status"] ?: @[];
NSString *notMe = data[@"not_me"] ?: @"";
// 2️⃣ Defect matrix expansion
NSArray *defectMatrix = data[@"defect_matrix"];
if ([defectMatrix isKindOfClass:[NSArray class]]) {
for (NSDictionary *entry in defectMatrix) {
NSArray *locationIds = entry[@"location_id"];
if (![locationIds isKindOfClass:[NSArray class]] || locationIds.count == 0) {
continue;
}
NSArray *category = entry[@"category"] ?: @[];
NSDictionary *settings = @{
@"category": category,
@"priority": priority,
@"contractor": contractor,
@"new_issue_status": newIssueStatus,
@"start_work_status": startWorkStatus,
@"sub_contractor": subContractor,
@"location_status": locationStatus,
@"not_me": notMe
};
// 3️⃣ Save per location_id
for (id locationId in locationIds) {
NSString *locationKey =
[NSString stringWithFormat:@"LOCATION_SETTINGS_%@", locationId];
[LocalStorage saveDictionary:settings forKey:locationKey];
}
}
}
next();
}];
}];
// 3️⃣ Add image download task as the last task
[tasks addObject:^(void (^next)(void)) {
if (selectedUnits.count == 0) {
next();
return;
}
[self fetchOfflineImageListForSelectedUnits:selectedUnits completion:^(NSArray<NSURL *> *urls, NSError *error) {
if (error || urls.count == 0) {
next();
return;
}
[self downloadImagesSequentially:urls progress:^(NSInteger completedImages, NSInteger totalImages) {
// Map image progress into overall progress
NSInteger overallCompleted = 6 + completedImages;
NSInteger overallTotal = 6 + urls.count;
if (progress) progress(overallCompleted, overallTotal);
} next:^{
next();
}];
}];
}];
// 4️⃣ Sequential executor
__block NSInteger currentTaskIndex = 0;
void (^runNextTask)(void);
runNextTask = ^{
if (currentTaskIndex >= tasks.count) {
[self saveLastOfflineSyncDate];
if (completion) completion(YES);
return;
}
void (^task)(void (^next)(void)) = tasks[currentTaskIndex];
currentTaskIndex++;
// Update progress for API tasks
if (progress && currentTaskIndex <= 6) {
NSInteger overallTotal = 6 + selectedUnits.count;
progress(currentTaskIndex, overallTotal);
}
task(runNextTask);
};
runNextTask();
}
#pragma mark - Image Download Helpers
- (NSArray *)buildImageSyncPayloadFromUnits:(NSArray *)units {
NSMutableDictionary<NSString *, NSMutableArray *> *grouped = @{}.mutableCopy;
for (NSDictionary *unit in units) {
NSString *projectId = unit[@"project_id"];
NSString *planId = unit[@"plan_id"];
if (!projectId || !planId) continue;
if (!grouped[projectId]) grouped[projectId] = [NSMutableArray array];
[grouped[projectId] addObject:planId];
}
NSMutableArray *result = [NSMutableArray array];
for (NSString *projectId in grouped) {
[result addObject:@{ @"project_id": projectId, @"plan_id": grouped[projectId] }];
}
return result;
}
- (void)fetchOfflineImageListForSelectedUnits:(NSArray *)units
completion:(void (^)(NSArray<NSURL *> *, NSError *))completion {
NSArray *payload = [self buildImageSyncPayloadFromUnits:units];
if (payload.count == 0) {
completion(@[], nil);
return;
}
[APIClient requestArrayImage:payload completion:^(NSDictionary *res, NSError *err) {
if (err) {
completion(nil, err);
return;
}
NSMutableOrderedSet<NSURL *> *urls = [NSMutableOrderedSet orderedSet];
NSDictionary *fileImage = res[@"Data"][@"file_image"];
for (NSDictionary *img in fileImage[@"plan_image"]) {
if (img[@"link"]) [urls addObject:[NSURL URLWithString:img[@"link"]]];
}
for (NSDictionary *img in fileImage[@"issue_image"]) {
if (img[@"link"]) [urls addObject:[NSURL URLWithString:img[@"link"]]];
}
completion(urls.array, nil);
}];
}
- (void)downloadImagesSequentially:(NSArray<NSURL *> *)urls
progress:(void (^)(NSInteger completed, NSInteger total))progress
next:(void (^)(void))next {
__block NSInteger index = 0;
NSInteger total = urls.count;
if (total == 0) {
if (next) next();
return;
}
__block void (^downloadNext)(void);
__weak __block void (^weakDownloadNext)(void);
downloadNext = ^{
if (index >= total) {
if (next) next();
return;
}
NSURL *url = urls[index];
[APIClient downloadAndCacheImage:url completion:^(BOOL success) {
index++;
if (progress) progress(index, total);
if (weakDownloadNext) weakDownloadNext();
}];
};
weakDownloadNext = downloadNext;
downloadNext();
}
- (void)saveLastOfflineSyncDate {
NSDate *now = [NSDate date];
NSTimeInterval timestamp = [now timeIntervalSince1970];
[LocalStorage saveObject:@(timestamp)
forKey:kLastOfflineSyncDateKey];
}
@end
......@@ -8,32 +8,32 @@
<key>BinaryPath</key>
<string>QmsPluginFramework.framework/QmsPluginFramework</string>
<key>LibraryIdentifier</key>
<string>ios-arm64_x86_64-simulator</string>
<string>ios-arm64</string>
<key>LibraryPath</key>
<string>QmsPluginFramework.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>simulator</string>
</dict>
<dict>
<key>BinaryPath</key>
<string>QmsPluginFramework.framework/QmsPluginFramework</string>
<key>LibraryIdentifier</key>
<string>ios-arm64</string>
<string>ios-arm64_x86_64-simulator</string>
<key>LibraryPath</key>
<string>QmsPluginFramework.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>simulator</string>
</dict>
</array>
<key>CFBundlePackageType</key>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment