Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
W
weihan-plugin
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Wei Han
weihan-plugin
Commits
1323f31a
Commit
1323f31a
authored
Jan 28, 2026
by
Wei Han
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fixed drawer logic
parent
7735252a
Show whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
630 additions
and
87 deletions
+630
-87
AnnounceViewController.mm
...QmsPluginFramework/Announcement/AnnounceViewController.mm
+1
-1
AppointmentDetailsViewController.h
...nFramework/Appointment/AppointmentDetailsViewController.h
+0
-2
DashboardViewController.mm
...k/QmsPluginFramework/Dashboard/DashboardViewController.mm
+3
-2
DrawerController.mm
...inFramework/QmsPluginFramework/Drawer/DrawerController.mm
+12
-3
DrawerViewController.h
...ramework/QmsPluginFramework/Drawer/DrawerViewController.h
+3
-0
DrawerViewController.mm
...amework/QmsPluginFramework/Drawer/DrawerViewController.mm
+116
-33
HelpViewController.mm
...Framework/QmsPluginFramework/Drawer/HelpViewController.mm
+1
-1
NotificationViewController.h
...rk/QmsPluginFramework/Drawer/NotificationViewController.h
+3
-5
NotificationViewController.mm
...k/QmsPluginFramework/Drawer/NotificationViewController.mm
+167
-20
ProfileViewController.mm
...mework/QmsPluginFramework/Drawer/ProfileViewController.mm
+221
-19
SyncScreenViewController.mm
...ework/QmsPluginFramework/Sync/SyncScreenViewController.mm
+1
-1
APIClient.h
...ginFramework/QmsPluginFramework/Utilities/API/APIClient.h
+6
-0
APIClient.mm
...inFramework/QmsPluginFramework/Utilities/API/APIClient.mm
+96
-0
No files found.
framework/QmsPluginFramework/QmsPluginFramework/Announcement/AnnounceViewController.mm
View file @
1323f31a
...
...
@@ -330,7 +330,7 @@
#pragma mark - helpers
- (void)onBack {
[self dismissViewControllerAnimated:YES completion:nil
];
[self.navigationController popViewControllerAnimated:YES
];
}
@end
...
...
framework/QmsPluginFramework/QmsPluginFramework/Appointment/AppointmentDetailsViewController.h
View file @
1323f31a
...
...
@@ -4,8 +4,6 @@
@interface
AppointmentDetailsViewController
:
UIViewController
@property
(
nonatomic
,
strong
)
NSString
*
appointmentID
;
@property
(
nonatomic
,
strong
)
NSDictionary
*
appointmentData
;
@end
framework/QmsPluginFramework/QmsPluginFramework/Dashboard/DashboardViewController.mm
View file @
1323f31a
...
...
@@ -72,6 +72,8 @@
NSLog(@"dashboard.drawerController = %@", self.drawerController);
//[[OfflineSyncManager shared] clearOfflineQueue];
[[NSNotificationCenter defaultCenter] postNotificationName:@"ShowHiddenButtonNotification" object:@(YES)];
}
- (void)viewWillAppear:(BOOL)animated {
...
...
@@ -102,7 +104,7 @@
}];
dispatch_group_enter(group);
[APIClient requestAnnouncement:^(BOOL success, NSDictionary *data, NSError *error) {
[APIClient request
Client
Announcement:^(BOOL success, NSDictionary *data, NSError *error) {
if (!error) announcementData = data;
dispatch_group_leave(group);
}];
...
...
@@ -1431,7 +1433,6 @@
[self presentViewController:vc animated:YES completion:nil];
}
- (void)handleIssueTap:(UITapGestureRecognizer *)gesture {
UIView *tappedCard = gesture.view;
NSDictionary *issueData = objc_getAssociatedObject(tappedCard, "issueData");
...
...
framework/QmsPluginFramework/QmsPluginFramework/Drawer/DrawerController.mm
View file @
1323f31a
// DrawerController.m
#import "DrawerController.h"
#import "ProjectViewController.h"
#import "DrawerViewController.h"
NSString * const DrawerDidSelectItemNotification = @"DrawerDidSelectItemNotification";
@interface DrawerController () <UIGestureRecognizerDelegate>
@property (nonatomic, strong) ProjectViewController *mainVC;
@property (nonatomic, strong) UIViewController *mainVC;
@property (nonatomic, strong) UINavigationController *mainNav;
@property (nonatomic, strong) UIViewController *drawerVC;
@property (nonatomic, strong) UIView *overlayView;
@property (nonatomic, assign) BOOL drawerOpen;
...
...
@@ -27,12 +29,14 @@ NSString * const DrawerDidSelectItemNotification = @"DrawerDidSelectItemNotifica
if (!self) return nil;
// 1️⃣ Initialize DashboardViewController with props
_main
VC = [[ProjectViewController alloc] initWithClientID:clientID
ProjectViewController *root
VC = [[ProjectViewController alloc] initWithClientID:clientID
clientCode:clientCode
userToken:user_token];
// 2️⃣ Set back-reference to drawerController
_mainVC.drawerController = self;
rootVC.drawerController = self;
_mainNav = [[UINavigationController alloc] initWithRootViewController:rootVC];
_mainVC = _mainNav;
// 3️⃣ Save props locally
_ClientID = [clientID copy];
...
...
@@ -41,6 +45,11 @@ NSString * const DrawerDidSelectItemNotification = @"DrawerDidSelectItemNotifica
// 4️⃣ Set drawer VC and drawer width
_drawerVC = drawer;
if ([_drawerVC isKindOfClass:[DrawerViewController class]]) {
[(DrawerViewController *)_drawerVC attachDrawerController:self];
}
_drawerWidth = MIN([UIScreen mainScreen].bounds.size.width * 0.78, 360);
return self;
...
...
framework/QmsPluginFramework/QmsPluginFramework/Drawer/DrawerViewController.h
View file @
1323f31a
// DrawerViewController.h
#import <UIKit/UIKit.h>
#import "DrawerController.h"
NS_ASSUME_NONNULL_BEGIN
...
...
@@ -10,7 +11,9 @@ NS_ASSUME_NONNULL_BEGIN
@property
(
nonatomic
,
strong
)
NSString
*
profileEmail
;
@property
(
nonatomic
,
strong
)
NSURL
*
profileImageURL
;
@property
(
nonatomic
,
strong
)
NSArray
<
NSDictionary
*>
*
menuItems
;
// array of { @"title":..., @"action":... }
@property
(
nonatomic
,
weak
,
readonly
)
DrawerController
*
drawerController
;
-
(
void
)
attachDrawerController
:(
DrawerController
*
)
drawerController
;
@end
NS_ASSUME_NONNULL_END
framework/QmsPluginFramework/QmsPluginFramework/Drawer/DrawerViewController.mm
View file @
1323f31a
...
...
@@ -7,6 +7,7 @@
#import "NotificationViewController.h"
#import "HelpViewController.h"
#import "AnnounceViewController.h"
#import "ProjectViewController.h"
static NSString * const ProfileDidUpdateNotification = @"ProfileDidUpdateNotification";
...
...
@@ -17,7 +18,7 @@ static NSString * const ProfileDidUpdateNotification = @"ProfileDidUpdateNotific
@property (nonatomic, strong) UILabel *emailLabel;
@property (nonatomic, strong) UITableView *table;
@property (nonatomic, strong) UIView *headerContainer;
@property (nonatomic, weak, readwrite) DrawerController *drawerController;
@end
@implementation DrawerViewController
...
...
@@ -38,6 +39,12 @@ static NSString * const ProfileDidUpdateNotification = @"ProfileDidUpdateNotific
selector:@selector(onProfileUpdated:)
name:ProfileDidUpdateNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleShowHiddenButton:)
name:@"ShowHiddenButtonNotification"
object:nil];
}
- (void)dealloc {
...
...
@@ -157,21 +164,23 @@ static NSString * const ProfileDidUpdateNotification = @"ProfileDidUpdateNotific
}
if (!self.menuItems) {
self.menuItems = @[
@{@"title": @"Notification", @"action": @"notification", @"icon": @"bell.fill"},
@{@"title": @"Announcement", @"action": @"announcement", @"icon": @"megaphone"},
@{@"title": @"Help", @"action": @"help", @"icon": @"questionmark"},
@{@"title": @"Sync", @"action": @"sync", @"icon": @"arrow.trianglehead.2.clockwise.rotate.90"},
@{@"title": @"Logout", @"action": @"logout", @"icon": @"rectangle.portrait.and.arrow.right" }
];
self.menuItems = [@[
[@{@"title": @"Project List", @"action": @"list", @"icon": @"", @"hidden": @YES} mutableCopy],
[@{@"title": @"Notification", @"action": @"notification", @"icon": @"bell.fill"} mutableCopy],
[@{@"title": @"Announcement", @"action": @"announcement", @"icon": @"megaphone"} mutableCopy],
[@{@"title": @"Help", @"action": @"help", @"icon": @"questionmark"} mutableCopy],
[@{@"title": @"Sync", @"action": @"sync", @"icon": @"arrow.trianglehead.2.clockwise.rotate.90"} mutableCopy],
[@{@"title": @"Logout", @"action": @"logout", @"icon": @"rectangle.portrait.and.arrow.right"} mutableCopy]
] mutableCopy];
}
[self.table reloadData];
}
#pragma mark - Profile tap
- (void)profileTapped {
// Post a notification to navigate
NSLog(@"Sidemenu header tapped");
DrawerController *drawerController = self.drawerController;
[drawerController closeDrawer];
ProfileViewController *vc = [[ProfileViewController alloc] init];
vc.modalPresentationStyle = UIModalPresentationFullScreen;
[self presentViewController:vc animated:YES completion:nil];
...
...
@@ -179,7 +188,12 @@ static NSString * const ProfileDidUpdateNotification = @"ProfileDidUpdateNotific
#pragma mark - Table datasource/delegate
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.menuItems.count;
NSPredicate *predicate =
[NSPredicate predicateWithBlock:^BOOL(NSDictionary *item, NSDictionary *bindings) {
return ![item[@"hidden"] boolValue];
}];
return [[self.menuItems filteredArrayUsingPredicate:predicate] count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
...
...
@@ -194,7 +208,7 @@ static NSString * const ProfileDidUpdateNotification = @"ProfileDidUpdateNotific
cell.imageView.contentMode = UIViewContentModeScaleAspectFit;
}
NSDictionary *item =
self.menuItems
[indexPath.row];
NSDictionary *item =
[self visibleMenuItems]
[indexPath.row];
cell.textLabel.text = item[@"title"] ?: @"";
NSString *iconName = item[@"icon"];
...
...
@@ -212,49 +226,85 @@ static NSString * const ProfileDidUpdateNotification = @"ProfileDidUpdateNotific
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return 56; }
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSDictionary *item =
self.menuItems
[indexPath.row];
NSDictionary *item =
[self visibleMenuItems]
[indexPath.row];
NSString *action = item[@"action"];
[[NSNotificationCenter defaultCenter] postNotificationName:@"DrawerDidSelectItemNotification"
object:action];
}
- (NSArray *)visibleMenuItems {
NSPredicate *predicate =
[NSPredicate predicateWithBlock:^BOOL(NSDictionary *item, NSDictionary *bindings) {
return ![item[@"hidden"] boolValue];
}];
return [self.menuItems filteredArrayUsingPredicate:predicate];
}
- (void)showHiddenButton:(BOOL)show {
for (NSMutableDictionary *item in self.menuItems) {
if ([item[@"action"] isEqualToString:@"list"]) {
item[@"hidden"] = @(!show);
}
}
[self.table reloadData]; // ensures table updates immediately
}
- (void)handleShowHiddenButton:(NSNotification *)notification {
NSLog(@"hidden button toggled");
BOOL show = [notification.object boolValue];
[self showHiddenButton:show];
}
- (void)handleDrawerAction:(NSNotification *)notification {
NSString *action = notification.object;
NSLog(@"Drawer action: %@", action);
if ([action isEqualToString:@"notification"]) {
NSLog(@"navigating to Notification");
DrawerController *drawerController = self.drawerController;
NSLog(@"drawerController: %@", drawerController);
if (!drawerController) return;
NotificationViewController *nextVC = [[NotificationViewController alloc] init];
nextVC.modalPresentationStyle = UIModalPresentationFullScreen;
[self presentViewController:nextVC animated:YES completion:nil];
UIViewController *mainVC = drawerController.mainViewController;
UINavigationController *nav = nil;
if ([mainVC isKindOfClass:[UINavigationController class]]) {
nav = (UINavigationController *)mainVC;
} else {
NSLog(@"❌ mainVC is not UINavigationController");
return;
}
// Always close drawer first
[drawerController closeDrawer];
if ([action isEqualToString:@"list"]) {
ProjectViewController *vc = [[ProjectViewController alloc] init];
vc.drawerController = drawerController;
[nav setViewControllers:@[vc] animated:NO]; // reset stack to Project list
}
else if ([action isEqualToString:@"notification"]) {
NotificationViewController *vc = [[NotificationViewController alloc] init];
vc.drawerController = drawerController;
[nav pushViewController:vc animated:YES];
}
else if ([action isEqualToString:@"announcement"]) {
NSLog(@"navigating to Announcement");
AnnouncementViewController *nextVC = [[AnnouncementViewController alloc] init];
nextVC.modalPresentationStyle = UIModalPresentationFullScreen;
[self presentViewController:nextVC animated:YES completion:nil];
AnnouncementViewController *vc = [[AnnouncementViewController alloc] init];
[nav pushViewController:vc animated:YES];
}
else if ([action isEqualToString:@"help"]) {
NSLog(@"navigating to Help");
HelpViewController *nextVC = [[HelpViewController alloc] init];
nextVC.modalPresentationStyle = UIModalPresentationFullScreen;
[self presentViewController:nextVC animated:YES completion:nil];
HelpViewController *vc = [[HelpViewController alloc] init];
[nav pushViewController:vc animated:YES];
}
else if ([action isEqualToString:@"sync"]) {
NSLog(@"navigating to Sync");
SyncScreenViewController *nextVC = [[SyncScreenViewController alloc] init];
nextVC.modalPresentationStyle = UIModalPresentationFullScreen;
[self presentViewController:nextVC animated:YES completion:nil];
SyncScreenViewController *vc = [[SyncScreenViewController alloc] init];
[nav pushViewController:vc animated:YES];
}
else if ([action isEqualToString:@"logout"]) {
// [self performLogout];
NSLog(@"navigating to Logout");
[self logoutTapped];
}
}
- (void)onProfileUpdated:(NSNotification *)notification {
NSDictionary *info = notification.userInfo;
...
...
@@ -281,4 +331,37 @@ static NSString * const ProfileDidUpdateNotification = @"ProfileDidUpdateNotific
});
}
- (void)logoutTapped {
UIAlertController *alert =
[UIAlertController alertControllerWithTitle:@"Alert"
message:@"Are you sure you want to logout?"
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *cancel =
[UIAlertAction actionWithTitle:@"No"
style:UIAlertActionStyleCancel
handler:nil];
UIAlertAction *confirm =
[UIAlertAction actionWithTitle:@"Yes"
style:UIAlertActionStyleDestructive
handler:^(UIAlertAction * _Nonnull action) {
// Clear session
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"userToken"];
[[NSUserDefaults standardUserDefaults] synchronize];
// Exit profile
[self dismissViewControllerAnimated:YES completion:nil];
}];
[alert addAction:cancel];
[alert addAction:confirm];
[self presentViewController:alert animated:YES completion:nil];
}
- (void)attachDrawerController:(DrawerController *)drawerController {
_drawerController = drawerController;
}
@end
framework/QmsPluginFramework/QmsPluginFramework/Drawer/HelpViewController.mm
View file @
1323f31a
...
...
@@ -190,7 +190,7 @@
#pragma mark - Back
- (void)onBack {
[self
dismissViewControllerAnimated:YES completion:nil
];
[self
.navigationController popViewControllerAnimated:YES
];
}
@end
framework/QmsPluginFramework/QmsPluginFramework/Drawer/NotificationViewController.h
View file @
1323f31a
// NotificationViewController.h
#import "DrawerController.h"
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface
NotificationViewController
:
UIViewController
@property
(
nonatomic
,
weak
)
DrawerController
*
drawerController
;
@end
NS_ASSUME_NONNULL_END
framework/QmsPluginFramework/QmsPluginFramework/Drawer/NotificationViewController.mm
View file @
1323f31a
...
...
@@ -3,6 +3,12 @@
#import "NotificationViewController.h"
#import "APIClient.h"
#import "ImageCacheHelper.h"
#import <objc/runtime.h>
#import "APIConfig.h"
#import "AppointmentDetailsViewController.h"
#import "DashboardViewController.h"
static char kNotificationKey;
@interface NotificationViewController ()
// Header (existing)
...
...
@@ -15,6 +21,7 @@
@property (nonatomic, strong) UIStackView *stackView;
@property (nonatomic, strong) NSArray *notifications;
@property (nonatomic, strong) NSArray *projectList;
@property (nonatomic, strong) NSString *oldUnitId;
@end
@implementation NotificationViewController
...
...
@@ -22,6 +29,9 @@
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = UIColor.whiteColor;
if (self.oldUnitId) {
[APIConfig setDrawingPlanId:self.oldUnitId];
}
[self requestProjectList];
[self setupHeader];
...
...
@@ -157,7 +167,6 @@
[card addGestureRecognizer:tap];
card.userInteractionEnabled = YES;
UIImageView *icon = [[UIImageView alloc] init];
icon.translatesAutoresizingMaskIntoConstraints = NO;
icon.contentMode = UIViewContentModeScaleAspectFit;
...
...
@@ -172,15 +181,12 @@
icon.image = image;
UIImage *img = image;
icon.image = img;
// Remove old aspect ratio if exists
for (NSLayoutConstraint *c in icon.constraints) {
if (c.firstAttribute == NSLayoutAttributeHeight &&
c.secondAttribute == NSLayoutAttributeWidth) {
[icon removeConstraint:c];
}
}
CGFloat ratio = img.size.height / img.size.width;
NSLayoutConstraint *aspect =
...
...
@@ -266,6 +272,11 @@
BOOL unread = [item[@"read_status"] isEqualToString:@"0"];
unreadDot.hidden = !unread;
objc_setAssociatedObject(card,
&kNotificationKey,
item,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return card;
}
...
...
@@ -291,7 +302,7 @@
#pragma mark - helpers
- (void)onBack {
[self
dismissViewControllerAnimated:YES completion:nil
];
[self
.navigationController popViewControllerAnimated:YES
];
}
- (void)requestNotification {
...
...
@@ -399,22 +410,158 @@
}
- (void)onNotificationTap:(UITapGestureRecognizer *)tap {
NSLog(@"notification tapped");
UIView *card = tap.view;
// NSDictionary *item =
// objc_getAssociatedObject(card, @"notification");
//
// if (!item) return;
//
// NSString *type = item[@"type"];
//
// if ([type isEqualToString:@"appointment_reminder"]) {
// [self openAppointment:item];
// } else if ([type isEqualToString:@"rectification"]) {
// [self openRectification:item];
// } else {
// [self openGenericNotification:item];
// }
NSDictionary *item =
objc_getAssociatedObject(card, &kNotificationKey);
if (!item) return;
NSString *notificationType = item[@"type"];
NSDictionary *strData = item;
self.oldUnitId = [APIConfig drawingPlanId];
NSString *unitID = @"";
if (strData[@"draw_plan_id"]) {
unitID = strData[@"draw_plan_id"];
[APIConfig setDrawingPlanId:unitID];
}
// ---- PAYMENT REMINDER ----
if ([notificationType isEqualToString:@"payment_reminder"]) {
NSLog(@"➡️ payment_reminder tapped");
return;
}
// ---- APPOINTMENT ----
if ([notificationType isEqualToString:@"appointment_reminder"] ||
[notificationType isEqualToString:@"unattend_appt_reminder"]) {
NSString *appointmentID = @"";
if (strData[@"appointment_id"]) {
appointmentID = strData[@"appointment_id"];
}
[self openAppointmentWithUnit:unitID appointmentID:appointmentID];
return;
}
// ---- CLEARANCE LETTER ----
if ([notificationType isEqualToString:@"clearance_letter"]) {
NSString *projectID = @"";
if (strData[@"project_id"]) {
projectID = strData[@"project_id"];
}
[self openClearanceWithProject:projectID unitID:unitID];
return;
}
// ---- DEFAULT: ISSUE FLOW ----
NSString *issueID = @"";
if (strData[@"issue_id"]) {
issueID = strData[@"issue_id"];
}
[self openIssueWithUnit:unitID issueID:issueID];
}
#pragma mark - Navigation (ported from RN)
- (void)openAppointmentWithUnit:(NSString *)unitID appointmentID:(NSString *)appointmentID {
NSLog(@"➡️ Open Appointment | unit=%@ appt=%@", unitID, appointmentID);
[APIClient requestDashboardInfo:^(BOOL success, NSDictionary * _Nullable data, NSError * _Nonnull error) {
if (!success || !data) {
return;
}
id appointment = data[@"Data"][@"appointment"];
if (!appointment ||
appointment == [NSNull null] ||
![appointment isKindOfClass:[NSArray class]] ||
appointmentID.length == 0) {
return;
}
NSDictionary *matchedAppointment = nil;
for (NSDictionary *appt in appointment) {
if (![appt isKindOfClass:[NSDictionary class]]) {
continue;
}
NSString *apptID = appt[@"id"];
if ([apptID isKindOfClass:[NSString class]] &&
[apptID isEqualToString:appointmentID]) {
matchedAppointment = appt;
break;
}
}
if (!matchedAppointment) {
// No match found
return;
}
// 👉 Navigate to Appointment Details
dispatch_async(dispatch_get_main_queue(), ^{
AppointmentDetailsViewController *vc = [[AppointmentDetailsViewController alloc] init];
vc.modalPresentationStyle = UIModalPresentationFullScreen;
vc.appointmentData = matchedAppointment;
[self presentViewController:vc animated:YES completion:nil];
});
}];
}
- (void)openClearanceWithProject:(NSString *)projectID unitID:(NSString *)unitID {
NSLog(@"➡️ Open Clearance | project=%@ unit=%@", projectID, unitID);
NSArray *projects = self.projectList;
if (!projects || projects.count == 0) {
NSLog(@"❌ Project list empty");
return;
}
for (NSDictionary *project in projects) {
if (![project isKindOfClass:[NSDictionary class]]) continue;
NSString *projID = [NSString stringWithFormat:@"%@", project[@"id"]];
if (![projID isEqualToString:projectID]) continue;
NSArray *units = project[@"unit"];
if (![units isKindOfClass:[NSArray class]]) break;
for (NSDictionary *unit in units) {
if (![unit isKindOfClass:[NSDictionary class]]) continue;
NSString *uID = [NSString stringWithFormat:@"%@", unit[@"id"]];
if (![uID isEqualToString:unitID]) continue;
NSLog(@"✅ Found project %@ and unit %@", projectID, unitID);
dispatch_async(dispatch_get_main_queue(), ^{
DashboardViewController *vc = [[DashboardViewController alloc] init];
vc.projectCode = unit[@"name"] ?: @"";
vc.projectName = project[@"project_name"] ?: @"";
vc.projectId = projectID ?: @"";
vc.drawingPlanId = unitID ?: @"";
vc.projectLogo = project[@"logo"] ?: @"";
vc.lotId = unit[@"lot_id"] ?: @"";
vc.drawerController = self.drawerController;
vc.modalPresentationStyle = UIModalPresentationFullScreen;
[self.navigationController pushViewController:vc animated:YES];
});
return;
}
}
NSLog(@"❌ No matching project/unit found");
}
- (void)openIssueWithUnit:(NSString *)unitID issueID:(NSString *)issueID {
NSLog(@"➡️ Open Issue | unit=%@ issue=%@", unitID, issueID);
// TODO: push IssueDetailViewController
}
@end
framework/QmsPluginFramework/QmsPluginFramework/Drawer/ProfileViewController.mm
View file @
1323f31a
...
...
@@ -3,8 +3,9 @@
#import "ProfileViewController.h"
#import "APIClient.h"
#import "ImageCacheHelper.h"
#import <Photos/Photos.h>
@interface ProfileViewController ()
@interface ProfileViewController ()
<UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIScrollViewDelegate>
// Header
@property (nonatomic, strong) UIView *headerBar;
...
...
@@ -42,6 +43,13 @@
@property (nonatomic, strong) UIView *addLinkSheet;
@property (nonatomic, strong) UILabel *usernameLabel;
@property (nonatomic, strong) UIImage *selectedProfileImage;
@property (nonatomic, strong) NSData *selectedProfileImageData;
@property (nonatomic, strong) UIView *imageViewer;
@property (nonatomic, strong) UIScrollView *imageScrollView;
@property (nonatomic, strong) UIImageView *fullscreenImageView;
@end
@implementation ProfileViewController
...
...
@@ -167,9 +175,16 @@
// Profile Image
self.profileImageView = [[UIImageView alloc] init];
self.profileImageView.backgroundColor = UIColor.systemGray5Color;
self.profileImageView.layer.cornerRadius = imageSize / 2;
self.profileImageView.clipsToBounds = YES;
self.profileImageView.layer.cornerRadius = imageSize / 2;
self.profileImageView.translatesAutoresizingMaskIntoConstraints = NO;
self.profileImageView.contentMode = UIViewContentModeScaleAspectFill;
self.profileImageView.userInteractionEnabled = YES;
UITapGestureRecognizer *tap =
[[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(profileImageTapped)];
[self.profileImageView addGestureRecognizer:tap];
[self.profileSectionView addSubview:self.profileImageView];
// Edit Image Button
...
...
@@ -208,7 +223,7 @@
self.editNameButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.profileSectionView addSubview:self.editNameButton];
[self.editNameButton addTarget:self
action:@selector(
showAddLinkSheet
)
action:@selector(
editNamePressed
)
forControlEvents:UIControlEventTouchUpInside];
self.usernameLabel = [[UILabel alloc] init];
...
...
@@ -247,7 +262,7 @@
// Layout
[NSLayoutConstraint activateConstraints:@[
[self.profileImageView.topAnchor constraintEqualToAnchor:self.profileSectionView.topAnchor constant:
16
],
[self.profileImageView.topAnchor constraintEqualToAnchor:self.profileSectionView.topAnchor constant:
24
],
[self.profileImageView.centerXAnchor constraintEqualToAnchor:self.profileSectionView.centerXAnchor],
[self.profileImageView.widthAnchor constraintEqualToConstant:imageSize],
[self.profileImageView.heightAnchor constraintEqualToConstant:imageSize],
...
...
@@ -257,23 +272,23 @@
[self.editProfileImageButton.trailingAnchor constraintEqualToAnchor:self.profileImageView.trailingAnchor],
[self.editProfileImageButton.bottomAnchor constraintEqualToAnchor:self.profileImageView.bottomAnchor],
[self.nameLabel.topAnchor constraintEqualToAnchor:self.editProfileImageButton.bottomAnchor constant:
16
],
[self.nameLabel.topAnchor constraintEqualToAnchor:self.editProfileImageButton.bottomAnchor constant:
35
],
[self.nameLabel.centerXAnchor constraintEqualToAnchor:self.profileSectionView.centerXAnchor],
[self.editNameButton.centerYAnchor constraintEqualToAnchor:self.nameLabel.centerYAnchor],
[self.editNameButton.leadingAnchor constraintEqualToAnchor:self.nameLabel.trailingAnchor constant:6],
[self.usernameLabel.topAnchor constraintEqualToAnchor:self.nameLabel.bottomAnchor constant:1
2
],
[self.usernameLabel.topAnchor constraintEqualToAnchor:self.nameLabel.bottomAnchor constant:1
9
],
[self.usernameLabel.centerXAnchor constraintEqualToAnchor:self.profileSectionView.centerXAnchor],
[self.usernameLabel.heightAnchor constraintEqualToConstant:28],
[self.usernameLabel.widthAnchor constraintGreaterThanOrEqualToConstant:120],
[self.emailLabel.topAnchor constraintEqualToAnchor:self.usernameLabel.bottomAnchor constant:1
2
],
[self.emailLabel.topAnchor constraintEqualToAnchor:self.usernameLabel.bottomAnchor constant:1
6
],
[self.emailLabel.centerXAnchor constraintEqualToAnchor:self.profileSectionView.centerXAnchor],
[self.contactLabel.topAnchor constraintEqualToAnchor:self.emailLabel.bottomAnchor constant:
8
],
[self.contactLabel.topAnchor constraintEqualToAnchor:self.emailLabel.bottomAnchor constant:
12
],
[self.contactLabel.centerXAnchor constraintEqualToAnchor:self.profileSectionView.centerXAnchor],
[self.contactLabel.bottomAnchor constraintEqualToAnchor:self.profileSectionView.bottomAnchor constant:-
16
],
[self.contactLabel.bottomAnchor constraintEqualToAnchor:self.profileSectionView.bottomAnchor constant:-
32
],
]];
}
...
...
@@ -314,6 +329,16 @@
UIView *separator = [self separatorView];
[card addSubview:separator];
UIButton *switchAccountButton = [self settingsRowWithTitle:@"Switch Account"
icon:@"person.crop.circle.badge.plus"
textColor:UIColor.labelColor
action:@selector(switchAccountTapped)];
[card addSubview:switchAccountButton];
UIView *separator2 = [self separatorView];
[card addSubview:separator2];
// ---- Logout row ----
UIButton *logoutButton =
[self settingsRowWithTitle:@"Log Out"
...
...
@@ -323,9 +348,9 @@
[card addSubview:logoutButton];
// ---- Delete Account pill ----
self.deleteAccountButton = [
UIButton buttonWithType:UIButtonTypeSystem];
[self.deleteAccountButton setTitle:@"Delete My Account" forState:UIControlStateNormal];
[self.deleteAccountButton setTitleColor:UIColor.systemRedColor forState:UIControlStateNormal
];
self.deleteAccountButton = [
self settingsRowWithTitle:@"Delete My Account"
icon:@"person" textColor:UIColor.systemRedColor
action:@selector(deleteAccountTapped)
];
self.deleteAccountButton.backgroundColor =
[[UIColor systemRedColor] colorWithAlphaComponent:0.1];
self.deleteAccountButton.layer.cornerRadius = 14;
...
...
@@ -339,7 +364,7 @@
// ---- Layout ----
[NSLayoutConstraint activateConstraints:@[
// Title
[titleLabel.topAnchor constraintEqualToAnchor:self.settingsSectionView.topAnchor constant:
16
],
[titleLabel.topAnchor constraintEqualToAnchor:self.settingsSectionView.topAnchor constant:
24
],
[titleLabel.leadingAnchor constraintEqualToAnchor:self.settingsSectionView.leadingAnchor],
// Card
...
...
@@ -351,7 +376,7 @@
[changeLanguageButton.topAnchor constraintEqualToAnchor:card.topAnchor],
[changeLanguageButton.leadingAnchor constraintEqualToAnchor:card.leadingAnchor],
[changeLanguageButton.trailingAnchor constraintEqualToAnchor:card.trailingAnchor],
[changeLanguageButton.heightAnchor constraintEqualToConstant:
52
],
[changeLanguageButton.heightAnchor constraintEqualToConstant:
48
],
// Language text
[langLabel.centerYAnchor constraintEqualToAnchor:changeLanguageButton.centerYAnchor],
...
...
@@ -363,11 +388,23 @@
[separator.trailingAnchor constraintEqualToAnchor:card.trailingAnchor constant:-16],
[separator.heightAnchor constraintEqualToConstant:1],
// Switch Button row
[switchAccountButton.topAnchor constraintEqualToAnchor:separator.topAnchor],
[switchAccountButton.leadingAnchor constraintEqualToAnchor:card.leadingAnchor],
[switchAccountButton.trailingAnchor constraintEqualToAnchor:card.trailingAnchor],
[switchAccountButton.heightAnchor constraintEqualToConstant:48],
// Separator2
[separator2.topAnchor constraintEqualToAnchor:switchAccountButton.bottomAnchor],
[separator2.leadingAnchor constraintEqualToAnchor:card.leadingAnchor constant:16],
[separator2.trailingAnchor constraintEqualToAnchor:card.trailingAnchor constant:-16],
[separator2.heightAnchor constraintEqualToConstant:1],
// Logout row
[logoutButton.topAnchor constraintEqualToAnchor:separator.bottomAnchor],
[logoutButton.topAnchor constraintEqualToAnchor:separator
2
.bottomAnchor],
[logoutButton.leadingAnchor constraintEqualToAnchor:card.leadingAnchor],
[logoutButton.trailingAnchor constraintEqualToAnchor:card.trailingAnchor],
[logoutButton.heightAnchor constraintEqualToConstant:
52
],
[logoutButton.heightAnchor constraintEqualToConstant:
48
],
// Card bottom
[card.bottomAnchor constraintEqualToAnchor:logoutButton.bottomAnchor],
...
...
@@ -469,6 +506,32 @@
}];
}
- (void)uploadProfileImage {
if (!self.selectedProfileImageData) return;
[APIClient uploadProfileImage:self.selectedProfileImageData
name:self.nameLabel.text
completion:^(BOOL success,
NSDictionary *response,
NSError *error) {
if (!success || error) {
NSLog(@"❌ Upload failed: %@", error);
return;
}
NSDictionary *appData = response[@"AppData"];
if (![appData[@"status"] isEqualToString:@"success"]) {
NSLog(@"❌ API error: %@", appData[@"message"]);
return;
}
NSLog(@"✅ Profile image updated");
[self requestProfile];
}];
}
- (UIView *)separatorView {
UIView *view = [[UIView alloc] init];
view.backgroundColor = UIColor.systemGray4Color;
...
...
@@ -510,7 +573,6 @@
[self presentViewController:alert animated:YES completion:nil];
}
- (void)deleteAccountTapped {
NSString *message =
...
...
@@ -652,7 +714,7 @@
return sheet;
}
- (void)
showAddLinkSheet
{
- (void)
editNamePressed
{
self.dimmedOverlay = [self buildDimmedOverlay];
self.addLinkSheet = [self buildAddLinkSheet];
...
...
@@ -730,8 +792,148 @@
}];
}
#pragma mark - Profile Picture
- (void)editProfileImageTapped {
NSLog(@"Edit profile image tapped");
UIAlertController *sheet =
[UIAlertController alertControllerWithTitle:@"Change Profile Picture"
message:nil
preferredStyle:UIAlertControllerStyleActionSheet];
UIAlertAction *camera =
[UIAlertAction actionWithTitle:@"Camera"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * _Nonnull action) {
[self openImagePicker:UIImagePickerControllerSourceTypeCamera];
}];
UIAlertAction *gallery =
[UIAlertAction actionWithTitle:@"Photo Library"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * _Nonnull action) {
[self openImagePicker:UIImagePickerControllerSourceTypePhotoLibrary];
}];
UIAlertAction *cancel =
[UIAlertAction actionWithTitle:@"Cancel"
style:UIAlertActionStyleCancel
handler:nil];
[sheet addAction:camera];
[sheet addAction:gallery];
[sheet addAction:cancel];
[self presentViewController:sheet animated:YES completion:nil];
}
- (void)openImagePicker:(UIImagePickerControllerSourceType)type {
if (![UIImagePickerController isSourceTypeAvailable:type]) {
return;
}
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
picker.sourceType = type;
picker.delegate = self;
picker.allowsEditing = YES;
[self presentViewController:picker animated:YES completion:nil];
}
- (void)imagePickerController:(UIImagePickerController *)picker
didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey,id> *)info {
UIImage *image = info[UIImagePickerControllerEditedImage]
?: info[UIImagePickerControllerOriginalImage];
self.selectedProfileImage = image;
// JPEG compression (RN used image/jpeg)
self.selectedProfileImageData = UIImageJPEGRepresentation(image, 0.85);
// Preview immediately
self.profileImageView.image = image;
[picker dismissViewControllerAnimated:YES completion:^{
[self uploadProfileImage];
}];
}
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
[picker dismissViewControllerAnimated:YES completion:nil];
}
- (void) changeLanguageTapped {
NSLog(@"Change Language Tapped");
}
-(void) switchAccountTapped{
NSLog(@"Switch Account Tapped");
}
- (void)profileImageTapped {
if (!self.profileImageView.image) return;
self.imageViewer = [[UIView alloc] initWithFrame:self.view.bounds];
self.imageViewer.backgroundColor = UIColor.blackColor;
self.imageViewer.alpha = 0;
[self.view addSubview:self.imageViewer];
self.imageScrollView =
[[UIScrollView alloc] initWithFrame:self.imageViewer.bounds];
self.imageScrollView.minimumZoomScale = 1.0;
self.imageScrollView.maximumZoomScale = 4.0;
self.imageScrollView.delegate = self;
[self.imageViewer addSubview:self.imageScrollView];
self.fullscreenImageView =
[[UIImageView alloc] initWithImage:self.profileImageView.image];
self.fullscreenImageView.frame = self.imageScrollView.bounds;
self.fullscreenImageView.contentMode = UIViewContentModeScaleAspectFit;
self.fullscreenImageView.userInteractionEnabled = YES;
[self.imageScrollView addSubview:self.fullscreenImageView];
// Dismiss tap
UITapGestureRecognizer *dismissTap =
[[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(dismissImageViewer)];
[self.imageViewer addGestureRecognizer:dismissTap];
// Double tap zoom
UITapGestureRecognizer *doubleTap =
[[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(doubleTapZoom:)];
doubleTap.numberOfTapsRequired = 2;
[self.imageScrollView addGestureRecognizer:doubleTap];
[UIView animateWithDuration:0.25 animations:^{
self.imageViewer.alpha = 1;
}];
}
#pragma mark - Zoom
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return self.fullscreenImageView;
}
- (void)doubleTapZoom:(UITapGestureRecognizer *)gesture {
if (self.imageScrollView.zoomScale > 1.0) {
[self.imageScrollView setZoomScale:1.0 animated:YES];
} else {
CGPoint point = [gesture locationInView:self.fullscreenImageView];
CGRect zoomRect = CGRectMake(point.x - 50, point.y - 50, 100, 100);
[self.imageScrollView zoomToRect:zoomRect animated:YES];
}
}
- (void)dismissImageViewer {
[UIView animateWithDuration:0.25 animations:^{
self.imageViewer.alpha = 0;
} completion:^(BOOL finished) {
[self.imageScrollView removeFromSuperview];
[self.imageViewer removeFromSuperview];
self.imageScrollView = nil;
self.imageViewer = nil;
self.fullscreenImageView = nil;
}];
}
@end
framework/QmsPluginFramework/QmsPluginFramework/Sync/SyncScreenViewController.mm
View file @
1323f31a
...
...
@@ -447,7 +447,7 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
#pragma mark - Helpers
- (void)onBack {
[self
dismissViewControllerAnimated:YES completion:nil
];
[self
.navigationController popViewControllerAnimated:YES
];
}
- (void)addTapToCard:(UIView *)card action:(SEL)selector {
...
...
framework/QmsPluginFramework/QmsPluginFramework/Utilities/API/APIClient.h
View file @
1323f31a
...
...
@@ -100,6 +100,12 @@ NS_ASSUME_NONNULL_BEGIN
+
(
void
)
requestAnnouncement
:(
void
(
^
)(
BOOL
success
,
NSDictionary
*
_Nullable
response
,
NSError
*
_Nullable
error
))
completion
;
+
(
void
)
uploadProfileImage
:(
NSData
*
)
imageData
name
:(
NSString
*
)
name
completion
:(
void
(
^
)(
BOOL
success
,
NSDictionary
*
_Nullable
response
,
NSError
*
_Nullable
error
))
completion
;
@end
NS_ASSUME_NONNULL_END
framework/QmsPluginFramework/QmsPluginFramework/Utilities/API/APIClient.mm
View file @
1323f31a
...
...
@@ -2320,6 +2320,101 @@
[task resume];
}
+ (void)uploadProfileImage:(NSData *)imageData
name:(NSString *)name
completion:(void(^)(BOOL success,
NSDictionary * _Nullable response,
NSError * _Nullable error))completion {
NSURL *url = [APIConfig urlWithPath:@"/framework/owner/auth/editUser"];
NSString *boundary = @"----ProfileImageBoundary";
NSMutableData *body = [NSMutableData data];
void (^appendString)(NSString *) = ^(NSString *string) {
[body appendData:[string dataUsingEncoding:NSUTF8StringEncoding]];
};
// name
appendString([NSString stringWithFormat:@"--%@\r\n", boundary]);
appendString(@"Content-Disposition: form-data; name=\"data[name]\"\r\n\r\n");
appendString(name ?: @"");
appendString(@"\r\n");
// language
appendString([NSString stringWithFormat:@"--%@\r\n", boundary]);
appendString(@"Content-Disposition: form-data; name=\"data[language]\"\r\n\r\n");
appendString(@"2");
appendString(@"\r\n");
// os
appendString([NSString stringWithFormat:@"--%@\r\n", boundary]);
appendString(@"Content-Disposition: form-data; name=\"data[os]\"\r\n\r\n");
appendString([APIConfig os]);
appendString(@"\r\n");
// information
appendString([NSString stringWithFormat:@"--%@\r\n", boundary]);
appendString(@"Content-Disposition: form-data; name=\"data[information]\"\r\n\r\n");
NSDictionary *info = [APIConfig deviceInfo];
NSData *jsonData =
[NSJSONSerialization dataWithJSONObject:info options:0 error:nil];
NSString *infoString =
[[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
appendString(infoString ?: @"");
appendString(@"\r\n");
// avatar
if (imageData) {
appendString([NSString stringWithFormat:@"--%@\r\n", boundary]);
appendString(@"Content-Disposition: form-data; name=\"data[avatar]\"; filename=\"profile.jpg\"\r\n");
appendString(@"Content-Type: image/jpeg\r\n\r\n");
[body appendData:imageData];
appendString(@"\r\n");
}
appendString([NSString stringWithFormat:@"--%@--\r\n", boundary]);
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";
[request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary]
forHTTPHeaderField:@"Content-Type"];
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
request.HTTPBody = body;
NSLog(@"🌍 upload profile image: %@", url.absoluteString);
NSURLSessionDataTask *task =
[[NSURLSession sharedSession] dataTaskWithRequest:request
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
if (error) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(NO, nil, error);
});
return;
}
NSError *parseError;
NSDictionary *json =
[NSJSONSerialization JSONObjectWithData:data options:0 error:&parseError];
dispatch_async(dispatch_get_main_queue(), ^{
if (parseError) {
completion(NO, nil, parseError);
} else {
completion(YES, json, nil);
}
});
}];
[task resume];
}
+ (void)requestNotification:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion {
if ([APIConfig handleOfflineForAPI:@"Notification" completion:completion]) {
...
...
@@ -2395,6 +2490,7 @@
}
// NSURL *url = [APIConfig urlWithPath:@"/announcement"];
//place holder, need to change back eventually
NSURL *url = [NSURL URLWithString:@"https://kitadev.commudesk.com/api/announcement?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImF0aGlyYWh6YWlkaUBjb252ZXAuY29tIiwicGFzc3dvcmQiOiIkMnkkMTAkNU9xYklqMnZ6UHJwckJrcVd5c3I2LmJCc1hVYS9Hd014eUhnL3RhUUhPUkNQcGdmTG8yakciLCJzdWIiOjIxNzAsImlzcyI6Imh0dHBzOi8va2l0YWRldi5jb21tdWRlc2suY29tL2FwaS9vd25lci9hdXRoL3Bhc3N3b3JkbGVzc19sb2dpbiIsImlhdCI6MTc2MDMxNTM1MiwiZXhwIjoyMDc1ODg0ODcyLCJuYmYiOjE3NjAzMTUzNTIsImp0aSI6IktoQmNyQnJEdDVNdDZlWmMifQ.JnxGbZ7hTeoOr6oibIzZMphgyKtTo2Aq9Efx6mrGfFA"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment