Compare commits

...

2 Commits

Author SHA1 Message Date
WaseemAbbasi22 39d938ddb5 one time task implemented 3 weeks ago
WaseemAbbasi22 9b160838a6 one time task implemented 3 weeks ago

@ -559,5 +559,15 @@ class URLs {
// Print/Report URLs
static get generateReport => "$_baseUrl/ReportPDF/GenerateReport";
//4104 attachment type lookup
//ontimeTask
//One Time Task Urls cleaner module new flow urls ....
static get getTaskType => "$_baseUrl/OneTimeTasks/GetTaskTypes";
static get scanQRCode => "$_baseUrl/OneTimeTasks/ScanQRCode";
static get proceedRequest => "$_baseUrl/OneTimeTasks/Proceed";
static get completeRequest => "$_baseUrl/OneTimeTasks/Complete";
static get getRequestById => "$_baseUrl/OneTimeTasks/GetById";
}

@ -176,6 +176,9 @@ class AllRequestsProvider extends ChangeNotifier {
// list = [16];
list.add(16);
}
if (context.userProvider.isHouseKeeper) {
list = [5];
}
return list;
}

@ -51,6 +51,7 @@ class UserProvider extends ChangeNotifier {
bool get isQualityUser => user!.type == UsersTypes.qualityUser;
bool get isCommissioningEngineer => user!.type == UsersTypes.commissioningEngineer;
bool get isHouseKeeper => user!.type == UsersTypes.houseKeeper;
VerifyOtpModel _verifyOtpModel = VerifyOtpModel();
SwipeTransaction _swipeTransactionModel = SwipeTransaction();

@ -45,11 +45,11 @@ class DashBoardProvider extends ChangeNotifier {
notifyListeners();
}
int _currentListIndex = 0;
int? _currentListIndex = 0;
int get currentListIndex => _currentListIndex;
int? get currentListIndex => _currentListIndex;
set currentListIndex(int value) {
set currentListIndex(int? value) {
_currentListIndex = value;
notifyListeners();
}
@ -114,7 +114,7 @@ class DashBoardProvider extends ChangeNotifier {
setTabs(userType: userType, context: context);
getDashBoardCount(usersType: userType);
resetRequestDataList();
getRequestDetail(usersType: userType, status: tabs[currentListIndex].tag, tabId: tabs[currentListIndex].id);
getRequestDetail(usersType: userType, status: tabs[currentListIndex!].tag, tabId: tabs[currentListIndex!].id);
// notifyListeners();
}
@ -148,6 +148,7 @@ class DashBoardProvider extends ChangeNotifier {
UsersTypes.engineer: URLs.engineerDashboardCountUrl,
UsersTypes.assessor: URLs.engineerDashboardCountUrl,
UsersTypes.assessorTl: URLs.engineerDashboardCountUrl,
UsersTypes.houseKeeper: URLs.engineerDashboardCountUrl,
};
url = urlMap[usersType] ?? URLs.nurseDashboardCountUrl;
Response response;
@ -281,6 +282,7 @@ class DashBoardProvider extends ChangeNotifier {
case UsersTypes.engineer:
case UsersTypes.assessor:
case UsersTypes.assessorTl:
case UsersTypes.houseKeeper:
if (status == 0) {
return URLs.engineerDashboardUpcoming;
} else if (status == 1 && tabId == 1) {
@ -447,6 +449,12 @@ class CategoryTabs {
return tabs;
}
if (userType == UsersTypes.houseKeeper) {
tabs.add(CategoryTabs(label: 'In Progress', tag: 2, id: 2, requestType: RequestTypeEnum.inProgress, icon: "dashboard/inprogress_icon", iconColor: AppColor.primary10));
tabs.add(CategoryTabs(label: 'Completed', tag: 4, id: 3, requestType: RequestTypeEnum.completed, icon: "complete_request", iconColor: AppColor.completedColor));
return tabs;
}
if (userType == UsersTypes.qualityUser) {
tabs.add(CategoryTabs(label: 'Open', tag: 1, id: 2, requestType: RequestTypeEnum.open, icon: "dashboard/open_icon", iconColor: AppColor.openColor));

@ -6,12 +6,14 @@ import 'package:nfc_manager/nfc_manager.dart';
import 'package:provider/provider.dart';
import 'package:test_sa/controllers/notification/firebase_notification_manger.dart';
import 'package:test_sa/controllers/notification/notification_manger.dart';
import 'package:test_sa/controllers/providers/api/all_requests_provider.dart';
import 'package:test_sa/controllers/providers/api/notifications_provider.dart';
import 'package:test_sa/controllers/providers/api/user_provider.dart';
import 'package:test_sa/controllers/providers/settings/setting_provider.dart';
import 'package:test_sa/dashboard_latest/dashboard_provider.dart';
import 'package:test_sa/dashboard_latest/widgets/app_bar_widget.dart';
import 'package:test_sa/dashboard_latest/widgets/progress_fragment.dart';
import 'package:test_sa/dashboard_latest/widgets/request_category_fragment_housekeeper.dart';
import 'package:test_sa/dashboard_latest/widgets/requests_fragment.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
@ -57,13 +59,28 @@ class _DashboardViewState extends State<DashboardView> {
_dashBoardProvider.setTabs(userType: userProvider.user!.type!, context: context);
_dashBoardProvider.getDashBoardCount(usersType: userProvider.user!.type!);
_dashBoardProvider.resetRequestDataList();
_dashBoardProvider.currentListIndex = 0;
_dashBoardProvider.getRequestDetail(
if (userProvider.isHouseKeeper) {
_dashBoardProvider.currentListIndex = 0;
_dashBoardProvider.getRequestDetail(
usersType: userProvider.user!.type!,
status: _dashBoardProvider.tabs[_dashBoardProvider.currentListIndex].tag,
tabId: _dashBoardProvider.tabs[_dashBoardProvider.currentListIndex].id,
isHighPriority: _dashBoardProvider.tabs[_dashBoardProvider.currentListIndex].isHighPriority,
isOverdue: _dashBoardProvider.tabs[_dashBoardProvider.currentListIndex].isOverDue);
showLoader: true,
status: _dashBoardProvider.tabs[_dashBoardProvider.currentListIndex!].tag,
tabId: _dashBoardProvider.tabs[_dashBoardProvider.currentListIndex!].id,
// isAscending: _dashBoardProvider.isAscending,
);
} else {
_dashBoardProvider.currentListIndex = null;
Provider.of<AllRequestsProvider>(context, listen: false)
..reset()
..getAllRequests(context);
}
// _dashBoardProvider.currentListIndex = 0;
// _dashBoardProvider.getRequestDetail(
// usersType: userProvider.user!.type!,
// status: _dashBoardProvider.tabs[_dashBoardProvider.currentListIndex].tag,
// tabId: _dashBoardProvider.tabs[_dashBoardProvider.currentListIndex].id,
// isHighPriority: _dashBoardProvider.tabs[_dashBoardProvider.currentListIndex].isHighPriority,
// isOverdue: _dashBoardProvider.tabs[_dashBoardProvider.currentListIndex].isOverDue);
}
void getInitialData() {
@ -95,7 +112,7 @@ class _DashboardViewState extends State<DashboardView> {
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent && !_dashBoardProvider.isDetailLoading) {
_dashBoardProvider.pageNum = _dashBoardProvider.pageNum + 1;
_dashBoardProvider.getRequestDetail(
usersType: user.type!, showLoader: false, status: _dashBoardProvider.tabs[_dashBoardProvider.currentListIndex].tag, tabId: _dashBoardProvider.tabs[_dashBoardProvider.currentListIndex].id);
usersType: user.type!, showLoader: false, status: _dashBoardProvider.tabs[_dashBoardProvider.currentListIndex??0].tag, tabId: _dashBoardProvider.tabs[_dashBoardProvider.currentListIndex??0].id);
}
});
}
@ -132,9 +149,14 @@ class _DashboardViewState extends State<DashboardView> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
16.height, ProgressFragment(), 16.height,
16.height,
ProgressFragment(),
16.height,
// const RequestsFragment(),
const RequestCategoryFragment()
context.userProvider.isHouseKeeper ? const RequestCategoryFragmentHouseKeeper() : const RequestCategoryFragment()
// const RequestCategoryFragment()
],
),
),

@ -194,7 +194,7 @@ class _AppBarWidgetState extends State<AppBarWidget> with TickerProviderStateMix
dashBoardProvider.setTabs(userType: settingProvider.user!.type!, context: context);
dashBoardProvider.getDashBoardCount(usersType: settingProvider.user!.type!);
dashBoardProvider.resetRequestDataList();
dashBoardProvider.getRequestDetail(usersType: settingProvider.user!.type!, status: dashBoardProvider.tabs[dashBoardProvider.currentListIndex].tag);
dashBoardProvider.getRequestDetail(usersType: settingProvider.user!.type!, status: dashBoardProvider.tabs[dashBoardProvider.currentListIndex??0].tag);
});
} else {
ApiManager.instance.assetGroup = tempGroup;

@ -232,7 +232,14 @@ class _ProgressFragmentState extends State<ProgressFragment> with SingleTickerPr
ChartData('In Progress', snapshot.dashboardCount?.data?.countInprogress?.toDouble() ?? 0.0, AppColor.inProgressChartColor, "dashboard/inprogress_icon"),
ChartData('Closed', snapshot.dashboardCount?.data?.countComplete?.toDouble() ?? 0.0, AppColor.completeChartColor, "dashboard/completed_icon"),
];
} else {
}else if (_userProvider.isHouseKeeper) {
total = (snapshot.dashboardCount?.data?.countClose ?? 0) + (snapshot.dashboardCount?.data?.countInprogress ?? 0) + (snapshot.dashboardCount?.data?.countOpen ?? 0);
chartData = [
ChartData('In Progress', snapshot.dashboardCount?.data?.countInprogress?.toDouble() ?? 0.0, AppColor.inProgressChartColor, "dashboard/inprogress_icon"),
ChartData('Completed', snapshot.dashboardCount?.data?.countClose?.toDouble() ?? 0.0, AppColor.completeChartColor, "dashboard/completed_icon"),
];
}
else {
total = (snapshot.dashboardCount?.data?.countComplete ?? 0) + (snapshot.dashboardCount?.data?.countInprogress ?? 0) + (snapshot.dashboardCount?.data?.countOpen ?? 0);
chartData = [
ChartData('Open', snapshot.dashboardCount?.data?.countOpen?.toDouble() ?? 0.0, AppColor.openChartColor, "dashboard/open_icon"),

@ -5,6 +5,7 @@ import 'package:test_sa/dashboard_latest/dashboard_provider.dart';
import 'package:test_sa/dashboard_latest/widgets/request_category_list.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/enums/user_types.dart';
@ -76,8 +77,8 @@ class _RequestCategoryFragmentState extends State<RequestCategoryFragment> with
dashboardProvider.getRequestDetail(
usersType: userType ?? UsersTypes.normal_user,
showLoader: true,
status: tabs[dashboardProvider.currentListIndex].tag,
tabId: tabs[dashboardProvider.currentListIndex].id,
status: tabs[dashboardProvider.currentListIndex??0].tag,
tabId: tabs[dashboardProvider.currentListIndex??0].id,
isAscending: isAscending,
);
}
@ -91,7 +92,7 @@ class _RequestCategoryFragmentState extends State<RequestCategoryFragment> with
// Check if user is FMS Engineer using SettingProvider
final bool isFMSEngineer = user?.type == UsersTypes.engineer && settingProvider.isUserFMS;
final bool isHouseKeeper = user?.type == UsersTypes.houseKeeper;
return Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
getTabs(
requestsProvider: dashboardProvider,
@ -102,7 +103,7 @@ class _RequestCategoryFragmentState extends State<RequestCategoryFragment> with
opacity: _headerFadeAnimation,
child: SlideTransition(
position: _headerSlideAnimation,
child: !isNurse && dashboardProvider.tabs.isNotEmpty && dashboardProvider.tabs[dashboardProvider.currentListIndex].tag == 0
child: !isNurse && dashboardProvider.tabs.isNotEmpty && dashboardProvider.tabs[dashboardProvider.currentListIndex??0].tag == 0
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -125,7 +126,23 @@ class _RequestCategoryFragmentState extends State<RequestCategoryFragment> with
)
: const SizedBox(),
],
).paddingOnly(start: 14, end: 12)
).paddingOnly(start: 14, end: 12):
isHouseKeeper
? Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
'All Tasks'.addTranslation.heading4(context),
'add_icon'
.toSvgAsset(
width: 24,
height: 24,
)
.onPress(() {
Navigator.of(context).pushNamed('/cleaner_search');
}),
],
).paddingOnly(start: 16, end: 16)
: Row(
spacing: 16,
children: [

@ -0,0 +1,242 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:test_sa/dashboard_latest/dashboard_provider.dart';
import 'package:test_sa/dashboard_latest/widgets/request_category_list.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/enum_extensions.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/enums/user_types.dart';
import 'package:test_sa/models/new_models/dashboard_detail.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/tab_button.dart';
import 'package:test_sa/views/widgets/loaders/no_data_found.dart';
/// Separate component for HouseKeeper role
/// HouseKeeper has simpler requirements:
/// - Single-select only (no multi-select)
/// - Uses getRequestDetail API only
/// - Default shows first category (In Progress)
class RequestCategoryFragmentHouseKeeper extends StatefulWidget {
const RequestCategoryFragmentHouseKeeper({Key? key}) : super(key: key);
@override
State<RequestCategoryFragmentHouseKeeper> createState() => _RequestCategoryFragmentHouseKeeperState();
}
class _RequestCategoryFragmentHouseKeeperState extends State<RequestCategoryFragmentHouseKeeper> with SingleTickerProviderStateMixin {
bool isAscending = false;
late AnimationController _animationController;
late Animation<double> _headerFadeAnimation;
late Animation<Offset> _headerSlideAnimation;
final ScrollController _scrollController = ScrollController();
// int _selectedIndex = 0; // Default first tab
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
);
// Header animation (appears first, faster)
_headerFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.4, curve: Curves.easeOut),
),
);
_headerSlideAnimation = Tween<Offset>(
begin: const Offset(0.0, -0.3),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.5, curve: Curves.easeOutCubic),
),
);
_animationController.forward();
// Load first category data on init
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadCategoryData(0);
});
}
@override
void dispose() {
_animationController.dispose();
_scrollController.dispose();
super.dispose();
}
void _loadCategoryData(int index) {
final dashboardProvider = Provider.of<DashBoardProvider>(context, listen: false);
List<CategoryTabs> tabs = CategoryTabs.getTabs(userType: UsersTypes.houseKeeper, context: context);
if (index >= 0 && index < tabs.length) {
dashboardProvider.resetRequestDataList();
dashboardProvider.getRequestDetail(
usersType: UsersTypes.houseKeeper,
showLoader: true,
status: tabs[index].tag,
tabId: tabs[index].id,
isAscending: isAscending,
);
}
}
void _toggleSort(DashBoardProvider dashboardProvider) {
setState(() {
isAscending = !isAscending;
});
// Reload current category with new sort order
_loadCategoryData(dashboardProvider.currentListIndex!);
}
@override
Widget build(BuildContext context) {
return Consumer<DashBoardProvider>(builder: (context, dashboardProvider, child) {
List<CategoryTabs> tabs = CategoryTabs.getTabs(userType: UsersTypes.houseKeeper, context: context);
print('i am here..');
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// Tabs
_buildTabs(tabs, dashboardProvider),
// Header with sort
FadeTransition(
opacity: _headerFadeAnimation,
child: SlideTransition(
position: _headerSlideAnimation,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
'All Tasks'.addTranslation.heading4(context),
'add_icon'.toSvgAsset(width: 24, height: 24).onPress(() {
Navigator.of(context).pushNamed('/cleaner_search');
}),
// Transform.flip(
// flipY: isAscending,
// child: 'dashboard/sort_descending'.toSvgAsset(
// width: 24,
// height: 24,
// color: AppColor.headingTextColor(context),
// ).onPress(()=>_toggleSort(dashboardProvider)),
// )
],
).paddingOnly(start: 16, end: 16),
),
),
// Data list
dashboardProvider.isDetailLoading
? Column(
mainAxisSize: MainAxisSize.max,
children: List.generate(3, (index) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 10.toScreenHeight, horizontal: 0),
child: const SizedBox().toRequestShimmer(context, dashboardProvider.isDetailLoading),
);
}),
).paddingOnly(start: 16, end: 16, top: 16)
: (dashboardProvider.requestDetailList == null || dashboardProvider.requestDetailList?.data?.isEmpty == true)
? const NoDataFound().paddingOnly(top: 80).center
: RequestCategoryList(
dashboardProvider.requestDetailList?.data ?? <Data>[],
dashboardProvider.isDetailLoading,
dashboardProvider.requestDetailList?.totalRows ?? 0,
),
],
);
});
}
Widget _buildTabs(List<CategoryTabs> tabs, DashBoardProvider dashboardProvider) {
// Calculate item width
final screenWidth = MediaQuery.of(context).size.width;
final isTablet = context.isTablet();
final visibleItemsWithPeek = isTablet ? 4.5 : 3.5;
const spacing = 4.0;
const horizontalPadding = 16.0;
final availableWidth = screenWidth - (horizontalPadding * 2);
final fullItems = visibleItemsWithPeek.floor();
final totalSpacing = (fullItems - 1) * spacing + (spacing * 0.5);
final itemWidth = (availableWidth - totalSpacing) / visibleItemsWithPeek;
return SizedBox(
height: itemWidth + 16,
child: ListView.separated(
controller: _scrollController,
scrollDirection: Axis.horizontal,
separatorBuilder: (cxt, index) => const SizedBox(width: 4),
padding: const EdgeInsets.only(bottom: 16, left: horizontalPadding, right: horizontalPadding),
itemBuilder: (cxt, index) {
// Animation
final delay = index * 0.08;
final animation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _animationController,
curve: Interval(
delay.clamp(0.0, 0.8),
(delay + 0.3).clamp(0.0, 1.0),
curve: Curves.easeOut,
),
),
);
final slideAnimation = Tween<Offset>(
begin: const Offset(-0.3, 0.0),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _animationController,
curve: Interval(
delay.clamp(0.0, 0.8),
(delay + 0.3).clamp(0.0, 1.0),
curve: Curves.easeOutCubic,
),
),
);
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: slideAnimation,
child: SizedBox(
width: itemWidth,
child: RequestTypeButton(
label: tabs[index].label,
loading: dashboardProvider.isDetailLoading && dashboardProvider.currentListIndex == index,
isSelected: dashboardProvider.currentListIndex == index,
count: dashboardProvider.getStatusCount(tabs[index].requestType),
onPressed: () {
setState(() {
isAscending = false;
// _selectedIndex = index;
});
dashboardProvider.currentListIndex = index;
_loadCategoryData(index);
},
icon: tabs[index].icon,
iconColor: tabs[index].iconColor,
),
),
),
);
},
itemCount: tabs.length,
),
);
}
}

@ -4,6 +4,7 @@ import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/new_models/dashboard_detail.dart';
import 'package:test_sa/modules/asset_delivery_module/pages/asset_delivery_item_view.dart';
import 'package:test_sa/modules/asset_inventory_module/pages/inventory_session_item_view.dart';
import 'package:test_sa/modules/cleaner_module/widgets/one_time_task_item_view.dart';
import 'package:test_sa/modules/demo_module/demo_request_item_view.dart';
import 'package:test_sa/modules/internal_audit_module/pages/equipment_internal_audit/equipment_internal_audit_item_view.dart';
import 'package:test_sa/modules/loan_module/pages/loan_equipment_item_view.dart';
@ -131,6 +132,8 @@ class _RequestCategoryListState extends State<RequestCategoryList> with SingleTi
return MedicalGasInspectionItemView(requestData: request);
case 15:
return DemoRequestItemView(requestData: request);
case 17:
return OneTimeTaskItemView(requestData: request);
default:
return Container(
height: 100,

@ -1,3 +1,4 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:localization/localization.dart';
import 'package:provider/provider.dart';
@ -5,6 +6,7 @@ import 'package:test_sa/controllers/providers/api/user_provider.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/new_views/swipe_module/dialoge/confirm_dialog.dart';
import 'package:test_sa/new_views/swipe_module/dialoge/info_dialog.dart';
import 'package:test_sa/views/full_screen_image_viewer.dart';
import '../controllers/providers/settings/setting_provider.dart';
import '../l10n/app_localizations.dart';
@ -43,6 +45,15 @@ extension BuildContextExtension on BuildContext {
builder: (BuildContext cxt) => InfoDialog(message: message, onTap: onTap, title: title, okTitle: okTitle, content: content),
);
void fullScreenImageViewer(Widget child) {
Navigator.of(this).push(
CupertinoPageRoute(
fullscreenDialog: true,
builder: (_) => FullScreenImageViewer(child),
),
);
}
Future showBottomSheet(Widget childWidget, {bool? isDismissible, String? title, bool showCancelButton = false}) => showModalBottomSheet(
context: this,
useSafeArea: true,

@ -134,6 +134,11 @@ class RequestsDetails {
int? transactionType;
int? numberOfAssets;
int? numberOfSites;
String? siteName;
String? buildingName;
String? departmentName;
String? floorName;
String? roomName;
RequestsDetails(
{this.id,
@ -163,7 +168,12 @@ class RequestsDetails {
this.poInternal,
this.poExternal,
this.date,
this.transactionType});
this.transactionType,
this.siteName,
this.buildingName,
this.departmentName,
this.floorName,
this.roomName});
RequestsDetails.fromJson(Map<String, dynamic> json) {
id = json['id'];
@ -194,6 +204,11 @@ class RequestsDetails {
siteTransferFrom = json['siteTransferFrom'];
siteTransferTo = json['siteTransferTo'];
transactionType = json['transactionType'];
siteName = json['siteName'];
buildingName = json['buildingName'];
departmentName = json['departmentName'];
floorName = json['floorName'];
roomName = json['roomName'];
}
Map<String, dynamic> toJson() {
@ -224,6 +239,11 @@ class RequestsDetails {
data['siteTransferFrom'] = siteTransferFrom;
data['siteTransferTo'] = siteTransferTo;
data['transactionType'] = transactionType;
data['siteName'] = siteName;
data['buildingName'] = buildingName;
data['departmentName'] = departmentName;
data['floorName'] = floorName;
data['roomName'] = roomName;
return data;
}
}

@ -5,5 +5,6 @@ enum UsersTypes {
assessor,
assessorTl,
qualityUser,
commissioningEngineer
commissioningEngineer,
houseKeeper
}

@ -63,6 +63,11 @@ class Data {
int? numberOfSites;
int? numberOfAssets;
String? nameOfType;
String? siteName;
String? buildingName;
String? departmentName;
String? floorName;
String? roomName;
Data(
{this.id,
@ -83,7 +88,13 @@ class Data {
this.requestType,
this.poExternal,
this.poInternal,
this.transactionNo});
this.transactionNo,
this.siteName,
this.buildingName,
this.departmentName,
this.floorName,
this.roomName,
});
Data.fromJson(Map<String, dynamic> json) {
id = json['id'];
@ -106,6 +117,11 @@ class Data {
poInternal = json['poInternal'];
poExternal = json['poExternal'];
nameOfType = json['nameOfType'];
siteName = json['siteName'];
buildingName = json['buildingName'];
departmentName = json['departmentName'];
floorName = json['floorName'];
roomName = json['roomName'];
}
Map<String, dynamic> toJson() {
@ -127,6 +143,11 @@ class Data {
data['numberOfAssets'] = numberOfAssets;
data['numberOfSites'] = numberOfSites;
data['nameOfType'] = nameOfType;
data['siteName'] = siteName;
data['buildingName'] = buildingName;
data['departmentName'] = departmentName;
data['floorName'] = floorName;
data['roomName'] = roomName;
return data;
}
}

@ -117,6 +117,8 @@ class User {
return UsersTypes.qualityUser;
case "R-201": // Quality User
return UsersTypes.commissioningEngineer;
case "R-204": // Quality User
return UsersTypes.houseKeeper;
default:
return null;
}

@ -0,0 +1,247 @@
class OneTimeTaskData {
int? id;
int? oneTimeSettingId;
OneTimeSetting? oneTimeSetting;
String? userId;
String? userName;
int? siteId;
String? siteName;
int? buildingId;
String? buildingName;
int? floorId;
String? floorName;
int? departmentID;
String? departmentName;
int? roomId;
String? roomName;
String? locationDescription;
String? startTime;
String? endTime;
int? statusId;
String? statusName;
int? statusValue;
List<Instruction>? instructions;
List<Attachment>? attachments;
String? createdDate;
String? createdBy;
String? imageRoom; // For backward compatibility - deprecated
List<String>? imageRooms; // For multiple captured images (max 2)
OneTimeTaskData({
this.id,
this.oneTimeSettingId,
this.oneTimeSetting,
this.userId,
this.userName,
this.siteId,
this.siteName,
this.buildingId,
this.buildingName,
this.floorId,
this.floorName,
this.departmentID,
this.departmentName,
this.roomId,
this.roomName,
this.locationDescription,
this.startTime,
this.endTime,
this.statusId,
this.statusName,
this.statusValue,
this.instructions,
this.attachments,
this.createdDate,
this.createdBy,
this.imageRoom,
this.imageRooms,
});
factory OneTimeTaskData.fromJson(Map<String, dynamic> json) {
return OneTimeTaskData(
id: json['id'],
oneTimeSettingId: json['oneTimeSettingId'],
oneTimeSetting: json['oneTimeSetting'] != null ? OneTimeSetting.fromJson(json['oneTimeSetting']) : null,
userId: json['userId'],
userName: json['userName'],
siteId: json['siteId'],
siteName: json['siteName'],
buildingId: json['buildingId'],
buildingName: json['buildingName'],
floorId: json['floorId'],
floorName: json['floorName'],
departmentID: json['departmentID'],
departmentName: json['departmentName'],
roomId: json['roomId'],
roomName: json['roomName'],
locationDescription: json['locationDescription'],
startTime: json['startTime'],
endTime: json['endTime'],
statusId: json['statusId'],
statusName: json['statusName'],
statusValue: json['statusValue'],
instructions: json['instructions'] != null
? (json['instructions'] as List).map((i) => Instruction.fromJson(i)).toList()
: null,
attachments: json['attachments'] != null
? (json['attachments'] as List).map((a) => Attachment.fromJson(a)).toList()
: null,
createdDate: json['createdDate'],
createdBy: json['createdBy'],
imageRoom: json['imageRoom'],
imageRooms: json['imageRooms'] != null ? List<String>.from(json['imageRooms']) : null,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'oneTimeSettingId': oneTimeSettingId,
'oneTimeSetting': oneTimeSetting?.toJson(),
'userId': userId,
'userName': userName,
'siteId': siteId,
'siteName': siteName,
'buildingId': buildingId,
'buildingName': buildingName,
'floorId': floorId,
'floorName': floorName,
'departmentID': departmentID,
'departmentName': departmentName,
'roomId': roomId,
'roomName': roomName,
'locationDescription': locationDescription,
'startTime': startTime,
'endTime': endTime,
'statusId': statusId,
'statusName': statusName,
'statusValue': statusValue,
'instructions': instructions?.map((i) => i.toJson()).toList(),
'attachments': attachments?.map((a) => a.toJson()).toList(),
'createdDate': createdDate,
'createdBy': createdBy,
'imageRoom': imageRoom,
'imageRooms': imageRooms,
};
}
}
class OneTimeSetting {
int? id;
int? taskTypeId;
String? taskTypeName;
int? instructionDescriptionId;
String? instructionDescriptionName;
int? sla;
OneTimeSetting({
this.id,
this.taskTypeId,
this.taskTypeName,
this.instructionDescriptionId,
this.instructionDescriptionName,
this.sla,
});
factory OneTimeSetting.fromJson(Map<String, dynamic> json) {
return OneTimeSetting(
id: json['id'],
taskTypeId: json['taskTypeId'],
taskTypeName: json['taskTypeName'],
instructionDescriptionId: json['instructionDescriptionId'],
instructionDescriptionName: json['instructionDescriptionName'],
sla: json['sla'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'taskTypeId': taskTypeId,
'taskTypeName': taskTypeName,
'instructionDescriptionId': instructionDescriptionId,
'instructionDescriptionName': instructionDescriptionName,
'sla': sla,
};
}
}
class Instruction {
int? id;
int? instructionTextId;
String? instructionText;
bool? isRequired;
bool? isDone;
String? comment;
Instruction({
this.id,
this.instructionTextId,
this.instructionText,
this.isRequired,
this.isDone,
this.comment,
});
factory Instruction.fromJson(Map<String, dynamic> json) {
return Instruction(
id: json['id'],
instructionTextId: json['instructionTextId'],
instructionText: json['instructionText'],
isRequired: json['isRequired'],
isDone: json['isDone'],
comment: json['comment'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'instructionTextId': instructionTextId,
'instructionText': instructionText,
'isRequired': isRequired,
'isDone': isDone,
'comment': comment,
};
}
}
class Attachment {
int? id;
String? name;
String? originalName;
String? fileName; // For backward compatibility
String? fileUrl; // For backward compatibility
String? fileType;
Attachment({
this.id,
this.name,
this.originalName,
this.fileName,
this.fileUrl,
this.fileType,
});
factory Attachment.fromJson(Map<String, dynamic> json) {
return Attachment(
id: json['id'],
name: json['name'],
originalName: json['originalName'],
fileName: json['fileName'],
fileUrl: json['fileUrl'],
fileType: json['fileType'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'originalName': originalName,
'fileName': fileName,
'fileUrl': fileUrl,
'fileType': fileType,
};
}
}

@ -0,0 +1,60 @@
class QrScanLocationData {
int? siteId;
String? siteName;
int? buildingId;
String? buildingName;
int? floorId;
String? floorName;
int? departmentId;
String? departmentName;
int? roomId;
String? roomName;
String? locationDescription;
QrScanLocationData({
this.siteId,
this.siteName,
this.buildingId,
this.buildingName,
this.floorId,
this.floorName,
this.departmentId,
this.departmentName,
this.roomId,
this.roomName,
this.locationDescription,
});
factory QrScanLocationData.fromJson(Map<String, dynamic> json) {
return QrScanLocationData(
siteId: json['siteId'],
siteName: json['siteName'],
buildingId: json['buildingId'],
buildingName: json['buildingName'],
floorId: json['floorId'],
floorName: json['floorName'],
departmentId: json['departmentId'],
departmentName: json['departmentName'],
roomId: json['roomId'],
roomName: json['roomName'],
locationDescription: json['locationDescription'],
);
}
Map<String, dynamic> toJson() {
return {
'siteId': siteId,
'siteName': siteName,
'buildingId': buildingId,
'buildingName': buildingName,
'floorId': floorId,
'floorName': floorName,
'departmentId': departmentId,
'departmentName': departmentName,
'roomId': roomId,
'roomName': roomName,
'locationDescription': locationDescription,
};
}
}

@ -0,0 +1,26 @@
import 'package:test_sa/models/base.dart';
class TaskType extends Base {
int? id;
String? taskTypeName;
TaskType({this.id, this.taskTypeName}) : super(identifier: id?.toString(), name: taskTypeName);
@override
bool operator ==(Object other) => identical(this, other) || other is TaskType && id != null && id == other.id;
@override
int get hashCode => id.hashCode;
Map<String, dynamic> toJson() {
return {"id": id, "taskTypeName": taskTypeName};
}
factory TaskType.fromJson(Map<String, dynamic>? parsedJson) {
if (parsedJson == null) return TaskType();
return TaskType(
id: parsedJson["id"],
taskTypeName: parsedJson["taskTypeName"],
);
}
}

@ -1,26 +1,23 @@
import 'package:flutter/material.dart';
import 'package:test_sa/controllers/providers/api/hospitals_provider.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/hospital.dart';
import 'package:test_sa/models/lookup.dart';
import 'package:test_sa/models/new_models/department_room_combined.dart';
import 'package:test_sa/modules/cleaner_module/pages/cleaner_search_list_view.dart';
import 'package:test_sa/modules/cleaner_module/models/task_type.dart';
import 'package:test_sa/modules/cleaner_module/models/qr_scan_location_data.dart';
import 'package:test_sa/modules/cleaner_module/models/one_time_task_data.dart';
import 'package:test_sa/modules/cleaner_module/provider/cleaner_provider.dart';
import 'package:test_sa/modules/cleaner_module/provider/fms_building_provider.dart';
import 'package:test_sa/modules/cleaner_module/provider/fms_floor_provider.dart';
import 'package:test_sa/modules/cleaner_module/provider/fms_location_description_provider.dart';
import 'package:test_sa/modules/cleaner_module/provider/task_type_provider.dart';
import 'package:test_sa/modules/cleaner_module/pages/one_time_task_view.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
import 'package:test_sa/new_views/common_widgets/single_item_drop_down_menu.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/providers/loading_list_notifier.dart';
import 'package:test_sa/models/new_models/building.dart';
import 'package:test_sa/models/new_models/floor.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/views/widgets/dialogs/confirm_dialog.dart';
import 'package:test_sa/views/widgets/item_views/info_text_widget.dart';
import 'package:test_sa/views/widgets/qr/scan_qr.dart';
class CleanerSearchFormView extends StatefulWidget {
static const String routeName = '/cleaner_search';
@ -32,161 +29,146 @@ class CleanerSearchFormView extends StatefulWidget {
}
class _CleanerSearchFormViewState extends State<CleanerSearchFormView> {
Hospital? selectedSite;
Building? selectedBuilding;
Floor? selectedFloor;
DepartmentRoomCombined? selectedDepartmentRoom;
Lookup? selectedLocationDescription;
late FocusNode _siteFocusNode;
final TextEditingController _siteController = TextEditingController();
List<Hospital> _siteOptions = [];
List<Building> _buildingOptions = [];
List<Floor> _floorOptions = [];
List<Lookup> _locationDescriptionOptions = [];
List<DepartmentRoomItem> _allDeptRoomList = [];
bool _isLoadingSites = false;
bool _isLoadingBuildings = false;
bool _isLoadingFloors = false;
bool _isLoadingLocationDescriptions = false;
bool _isSearching = false;
TaskType? selectedTaskType;
List<TaskType> _taskTypeOptions = [];
bool _isLoadingTaskTypes = false;
bool _isScanning = false;
bool _isProceeding = false;
QrScanLocationData? _scannedLocationData;
@override
void initState() {
super.initState();
_siteFocusNode = FocusNode();
_loadTaskTypes();
}
@override
void dispose() {
_siteController.dispose();
_siteFocusNode.dispose();
super.dispose();
}
Future<void> _searchSites(String query) async {
if (query.isEmpty) {
setState(() {
_siteOptions = [];
});
return;
}
Future<void> _loadTaskTypes() async {
setState(() {
_isLoadingSites = true;
_isLoadingTaskTypes = true;
});
try {
final sites = await HospitalsProvider().getHospitalsListByVal(searchVal: query);
final taskTypes = await TaskTypeProvider().getTaskTypesList();
setState(() {
_siteOptions = sites;
_isLoadingSites = false;
_taskTypeOptions = taskTypes;
_isLoadingTaskTypes = false;
});
} catch (e) {
setState(() {
_isLoadingSites = false;
_isLoadingTaskTypes = false;
_taskTypeOptions = [];
});
_showErrorDialog(context, 'Failed to load task types. Please try again.');
}
}
Future<void> _loadBuildings(int siteId) async {
setState(() {
_isLoadingBuildings = true;
});
Future<void> _onScanQRCode() async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const ScanQr()),
);
try {
final buildings = await FmsBuildingProvider().getBuildingsListBySiteId(siteId);
if (result != null && result is String && result.isNotEmpty) {
setState(() {
_buildingOptions = buildings;
_isLoadingBuildings = false;
});
} catch (e) {
setState(() {
_isLoadingBuildings = false;
_buildingOptions = [];
_isScanning = true;
});
try {
final qrResult = await CleanerProvider().scanQRCode(qrCode: result);
final isSuccess = qrResult["success"] ?? false;
final backendMessage = qrResult["message"] ?? "Unknown error occurred";
if (isSuccess && qrResult["data"] != null) {
// Store the scanned location data
setState(() {
_scannedLocationData = QrScanLocationData.fromJson(qrResult["data"]);
_isScanning = false;
});
} else {
setState(() {
_isScanning = false;
});
if (mounted) {
_showErrorDialog(context, backendMessage);
}
}
} catch (e) {
setState(() {
_isScanning = false;
});
if (mounted) {
_showErrorDialog(context, 'Failed to scan QR code. Please try again.');
}
}
}
}
Future<void> _loadFloors(int siteId, int buildingId) async {
setState(() {
_isLoadingFloors = true;
});
Future<void> _onProceed() async {
// Validate that task type and location data are available
if (selectedTaskType == null) {
_showErrorDialog(context, 'Please select a task type');
return;
}
try {
final floors = await FmsFloorProvider().getFloorsListBySiteAndBuilding(siteId, buildingId);
setState(() {
_floorOptions = floors;
_isLoadingFloors = false;
});
} catch (e) {
setState(() {
_isLoadingFloors = false;
_floorOptions = [];
});
if (_scannedLocationData == null) {
_showErrorDialog(context, 'Please scan a QR code first');
return;
}
}
Future<void> _loadLocationDescriptions(int siteId, int buildingId, int floorId) async {
setState(() {
_isLoadingLocationDescriptions = true;
_isProceeding = true;
});
try {
final response = await FmsLocationDescriptionProvider().getLocationDescriptionsList(siteId, buildingId, floorId);
setState(() {
_locationDescriptionOptions = response?.locationDescriptions ?? [];
_allDeptRoomList = response?.deptRoom ?? [];
_isLoadingLocationDescriptions = false;
});
} catch (e) {
setState(() {
_isLoadingLocationDescriptions = false;
_locationDescriptionOptions = [];
_allDeptRoomList = [];
});
}
}
List<DepartmentRoomCombined> _getDepartmentRoomCombinations() {
if (selectedLocationDescription == null || _allDeptRoomList.isEmpty) return [];
// Filter dept room list by selected location description
List<DepartmentRoomItem> filteredList = _allDeptRoomList.where((item) => item.locationDescriptionName == selectedLocationDescription!.name).toList();
// Convert to DepartmentRoomCombined
return filteredList
.map((item) => DepartmentRoomCombined(
deptId: item.deptId,
roomId: item.roomId,
deptName: item.deptName,
roomName: item.roomName,
locationDescriptionName: item.locationDescriptionName,
deptAndRoomName: item.deptAndRoomName,
))
.toList();
}
void _autoSelectDepartmentRoom() {
final combinations = _getDepartmentRoomCombinations();
if (combinations.isEmpty) {
setState(() {
selectedDepartmentRoom = null;
});
return;
}
final result = await CleanerProvider().proceedOneTimeTask(
oneTimeSettingId: selectedTaskType!.id ?? 0,
siteId: _scannedLocationData!.siteId ?? 0,
buildingId: _scannedLocationData!.buildingId ?? 0,
floorId: _scannedLocationData!.floorId ?? 0,
departmentId: _scannedLocationData!.departmentId ?? 0,
roomId: _scannedLocationData!.roomId ?? 0,
);
// If only one item, auto-fill
if (combinations.length == 1) {
setState(() {
selectedDepartmentRoom = combinations.first;
});
} else {
// If more than one, auto-select first item
final isSuccess = result["success"] ?? false;
final data = result["data"];
final message = result["message"] ?? "Unknown error occurred";
if (isSuccess && data != null) {
// Get the task ID from the response
final taskId = data['id'];
if (mounted && taskId != null) {
// Convert the data to OneTimeTaskData and navigate with the data
final taskData = OneTimeTaskData.fromJson(data);
// Navigate to OneTimeTaskView with the data
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => OneTimeTaskView(
taskId: taskId,
initialData: taskData,
),
),
);
}
} else {
if (mounted) {
_showErrorDialog(context, message);
}
}
} catch (e) {
if (mounted) {
_showErrorDialog(context, 'Failed to proceed. Please try again.');
}
} finally {
setState(() {
selectedDepartmentRoom = combinations.first;
_isProceeding = false;
});
}
}
@ -201,238 +183,88 @@ class _CleanerSearchFormViewState extends State<CleanerSearchFormView> {
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
spacing: 12,
children: [
// Site Autocomplete Field
Container(
padding: EdgeInsets.symmetric(horizontal: 16.toScreenWidth, vertical: 8.toScreenHeight),
decoration: BoxDecoration(
color: AppColor.dropDownBackground(context),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColor.border2Color(context), width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Site',
style: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 12, color: AppColor.floatingTextStyleColor(context)),
),
SizedBox(
height: 24.toScreenHeight, // Fixed height for consistency
child: Autocomplete<Hospital>(
optionsBuilder: (TextEditingValue textEditingValue) async {
if (textEditingValue.text.isEmpty) {
return const Iterable<Hospital>.empty();
}
await _searchSites(textEditingValue.text);
return _siteOptions;
},
displayStringForOption: (Hospital option) => option.name ?? "",
onSelected: (Hospital selected) async {
// _siteFocusNode.unfocus();
FocusManager.instance.primaryFocus?.unfocus();
setState(() {
selectedSite = selected;
_siteController.text = selected.name ?? '';
// Reset dependent dropdowns
selectedBuilding = null;
selectedFloor = null;
selectedLocationDescription = null;
selectedDepartmentRoom = null;
_buildingOptions = [];
_floorOptions = [];
_locationDescriptionOptions = [];
_allDeptRoomList = [];
});
// Load buildings for selected site
if (selected.id != null) {
await _loadBuildings(selected.id!);
}
},
fieldViewBuilder: (context, controller, focusNode, onFieldSubmitted) {
if (_siteController.text.isNotEmpty && controller.text.isEmpty) {
controller.text = _siteController.text;
}
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: TextField(
controller: controller,
// focusNode: _siteFocusNode,
focusNode: focusNode,
cursorColor: AppColor.loadingColor(context),
style: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 14, color: AppColor.textStyleColor(context)),
decoration: InputDecoration(
hintText: 'Search site...',
hintStyle: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 12, color: AppColor.labelTextStyleColor(context)),
filled: false,
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding: EdgeInsets.zero,
isDense: true,
),
onChanged: (value) {
_siteController.text = value;
},
),
),
if (_isLoadingSites)
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: AppColor.loadingColor(context)),
)
else if (controller.text.isNotEmpty)
GestureDetector(
onTap: () {
controller.clear();
_siteController.clear();
setState(() {
selectedSite = null;
selectedBuilding = null;
selectedFloor = null;
selectedLocationDescription = null;
selectedDepartmentRoom = null;
_siteOptions = [];
_buildingOptions = [];
_floorOptions = [];
_locationDescriptionOptions = [];
_allDeptRoomList = [];
});
},
child: Icon(
Icons.clear,
color: AppColor.icon2Color(context),
size: 20,
),
),
],
);
},
),
),
],
),
),
// Building Dropdown
SingleItemDropDownMenu<Building, NullableLoadingProvider>(
// Task Type Dropdown
SingleItemDropDownMenu<TaskType, NullableLoadingProvider>(
context: context,
title: 'Building',
initialValue: selectedBuilding,
title: 'Task Type',
initialValue: selectedTaskType,
showShadow: false,
showAsBottomSheet: true,
// backgroundColor: AppColor.fieldBgColor(context),
enabled: _buildingOptions.isNotEmpty,
loading: _isLoadingBuildings,
staticData: _buildingOptions,
onSelect: (value) async {
_siteFocusNode.unfocus();
backgroundColor: AppColor.fieldBgColor(context),
enabled: _taskTypeOptions.isNotEmpty,
loading: _isLoadingTaskTypes,
staticData: _taskTypeOptions,
onSelect: (value) {
setState(() {
_siteOptions = [];
selectedBuilding = value;
selectedFloor = null;
selectedLocationDescription = null;
selectedDepartmentRoom = null;
_floorOptions = [];
_locationDescriptionOptions = [];
_allDeptRoomList = [];
selectedTaskType = value;
});
// Load floors for selected building
if (value != null && selectedSite?.id != null && value.id != null) {
await _loadFloors(selectedSite!.id!, value.id!.toInt());
}
},
),
// Floor Dropdown
SingleItemDropDownMenu<Floor, NullableLoadingProvider>(
context: context,
title: 'Floor',
initialValue: selectedFloor,
showShadow: false,
showAsBottomSheet: true,
// backgroundColor: AppColor.fieldBgColor(context),
enabled: _floorOptions.isNotEmpty,
loading: _isLoadingFloors,
staticData: _floorOptions,
onSelect: (value) async {
setState(() {
selectedFloor = value;
selectedLocationDescription = null;
selectedDepartmentRoom = null;
_locationDescriptionOptions = [];
_allDeptRoomList = [];
});
// Load location descriptions for selected floor
if (value != null && selectedSite?.id != null && selectedBuilding?.id != null && value.id != null) {
await _loadLocationDescriptions(
selectedSite!.id!,
selectedBuilding!.id!.toInt(),
value.id!.toInt(),
);
}
},
// Scan QR Code Button
GestureDetector(
onTap: selectedTaskType != null ? _onScanQRCode : null,
child: Container(
height: 50,
alignment: Alignment.center,
decoration: BoxDecoration(
color: selectedTaskType != null ? AppColor.background(context) : Colors.grey[300],
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: selectedTaskType != null ? AppColor.borderColor(context) : Colors.grey,
width: 1,
),
),
padding: EdgeInsets.symmetric(horizontal: 16.toScreenWidth, vertical: 8.toScreenHeight),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
"scan_asset".toSvgAsset(
height: 22,
fit: BoxFit.fitHeight,
color: selectedTaskType != null ? AppColor.icon2Color(context) : Colors.grey,
),
8.width,
//we can change the implementation when merge with uiux branch
_scannedLocationData == null
? "Scan QR Code".bodyText(context).custom(
color: selectedTaskType != null ? AppColor.icon2Color(context) : Colors.grey,
)
: 'Re Scan QR Code'.bodyText(context).custom(
color: selectedTaskType != null ? AppColor.icon2Color(context) : Colors.grey,
),
],
),
),
),
// Location Description Dropdown
SingleItemDropDownMenu<Lookup, NullableLoadingProvider>(
context: context,
title: 'Location Description',
initialValue: selectedLocationDescription,
showShadow: false,
showAsBottomSheet: true,
// backgroundColor: AppColor.fieldBgColor(context),
enabled: _locationDescriptionOptions.isNotEmpty,
loading: _isLoadingLocationDescriptions,
staticData: _locationDescriptionOptions,
onSelect: (value) {
setState(() {
selectedLocationDescription = value;
selectedDepartmentRoom = null;
});
// Auto-select department-room based on location description
_autoSelectDepartmentRoom();
},
),
// Loading Indicator
if (_isScanning)
const Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(),
),
),
// Department & Room Combined Dropdown
SingleItemDropDownMenu<DepartmentRoomCombined, NullableLoadingProvider>(
context: context,
title: 'Department - Room',
initialValue: selectedDepartmentRoom,
showShadow: false,
showAsBottomSheet: true,
backgroundColor: AppColor.fieldBgColor(context),
enabled: _getDepartmentRoomCombinations().isNotEmpty,
staticData: _getDepartmentRoomCombinations(),
onSelect: (value) {
setState(() {
selectedDepartmentRoom = value;
});
},
),
// Scanned Location Data Card
if (_scannedLocationData != null && !_isScanning) _buildLocationDataCard(context),
],
).toShadowContainer(context).paddingOnly(top: 20, start: 16, end: 16),
),
),
// Footer Action Button
FooterActionButton.footerContainer(
context: context,
child: AppFilledButton(
buttonColor: AppColor.primary10,
label: context.translation.search,
buttonColor: _scannedLocationData != null && !_isProceeding ? AppColor.primary10 : Colors.grey,
label: 'Proceed',
maxWidth: true,
onPressed: _isSearching ? null : _onSubmit,
loading: _isSearching,
onPressed: (_scannedLocationData != null && !_isProceeding) ? _onProceed : null,
loading: _isProceeding,
),
),
],
@ -440,51 +272,74 @@ class _CleanerSearchFormViewState extends State<CleanerSearchFormView> {
);
}
Future<void> _onSubmit() async {
// Validate that at least site is selected
if (selectedSite == null) {
'Please select a site'.showToast;
return;
}
try {
setState(() {
_isSearching = true;
});
Widget _buildLocationDataCard(BuildContext context) {
final locationData = _scannedLocationData;
// Call the search API
final response = await CleanerProvider().searchPlanRecurrentTasksList(
pageSize: 10,
pageNumber: 1,
siteId: selectedSite?.id,
buildingId: selectedBuilding?.id?.toInt(),
floorId: selectedFloor?.id?.toInt(),
departmentId: selectedDepartmentRoom?.deptId,
roomId: selectedDepartmentRoom?.roomId,
);
if (locationData == null) {
return const SizedBox();
}
final results = response['data'] as List<dynamic>;
final totalRows = response['totalRows'] as int;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (locationData.siteName != null && locationData.siteName!.isNotEmpty)
InfoTextWidget(
label: 'Site', value: locationData.siteName,
// style: AppTextStyles.bodyText.copyWith(
// color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
// ),
),
if (locationData.buildingName != null && locationData.buildingName!.isNotEmpty)
InfoTextWidget(
label: 'Building',
value: locationData.buildingName,
),
if (locationData.floorName != null && locationData.floorName!.isNotEmpty)
InfoTextWidget(
label: 'Floor',
value: locationData.floorName,
),
if (locationData.departmentName != null && locationData.departmentName!.isNotEmpty)
InfoTextWidget(
label: 'Department',
value: locationData.departmentName,
),
if (locationData.roomName != null && locationData.roomName!.isNotEmpty)
InfoTextWidget(
label: 'Room',
value: locationData.roomName,
),
if (locationData.locationDescription != null && locationData.locationDescription!.isNotEmpty)
InfoTextWidget(
label: 'Location Description',
value: locationData.locationDescription,
),
// Text(
// 'Location Description: ${locationData.locationDescription}',
// style: AppTextStyles.bodyText.copyWith(
// color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
// ),
// softWrap: true,
// maxLines: null,
// ),
],
).paddingOnly(start: 16, end: 16, top: 16, bottom: 16),
],
).toShadowContainer(context, padding: 0);
}
// Navigate to search results page with the results
Navigator.of(context).pushNamed(
CleanerSearchListView.routeName,
arguments: {
'searchResults': results,
'totalRows': totalRows,
'selectedSite': selectedSite,
'selectedBuilding': selectedBuilding,
'selectedFloor': selectedFloor,
'selectedLocationDescription': selectedLocationDescription,
'selectedDepartmentRoom': selectedDepartmentRoom,
},
);
} catch (e) {
'Error searching'.showToast;
} finally {
setState(() {
_isSearching = false;
});
}
void _showErrorDialog(BuildContext context, String message) {
showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: message,
title: 'Error',
onTap: () => Navigator.pop(context),
),
);
}
}

@ -0,0 +1,346 @@
import 'package:flutter/material.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/modules/cleaner_module/models/one_time_task_data.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/views/widgets/images/files_list.dart';
import 'package:test_sa/modules/cm_module/cm_request_utils.dart';
import 'package:test_sa/views/widgets/item_views/info_header_widget.dart';
import 'package:test_sa/views/widgets/item_views/info_text_widget.dart';
import 'package:test_sa/views/widgets/timer/app_timer.dart';
import 'package:test_sa/models/timer_model.dart';
import 'dart:convert';
class OneTimeTaskPreviewScreen extends StatelessWidget {
static const String routeName = "/one_time_task_preview";
final OneTimeTaskData taskData;
final double totalWorkingHours;
final VoidCallback onConfirm;
const OneTimeTaskPreviewScreen({
Key? key,
required this.taskData,
required this.totalWorkingHours,
required this.onConfirm,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// Debug logging
print('DEBUG Preview Screen:');
print('imageRooms count: ${taskData.imageRooms?.length ?? 0}');
print('attachments count: ${taskData.attachments?.length ?? 0}');
// Determine which images to show: captured images (imageRooms) or API attachments
final hasImageRooms = taskData.imageRooms != null && taskData.imageRooms!.isNotEmpty;
final hasAttachments = taskData.attachments != null && taskData.attachments!.isNotEmpty;
return Scaffold(
appBar: const DefaultAppBar(
title: 'Preview',
),
body: SafeArea(
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Task Information Card
_buildTaskInfoCard(context),
16.height,
// Working Hours Card
if (totalWorkingHours > 0) ...[
_buildWorkingHoursCard(context),
16.height,
],
// Checklist Card
_buildChecklistCard(context),
16.height,
// Captured Images Card
if (hasImageRooms) ...[
_buildImagesCard(context),
16.height,
],
// Attachments Card (from API)
if (hasAttachments) ...[
_buildAttachmentsCard(context),
16.height,
],
],
),
),
),
// Footer with Confirm Button
FooterActionButton.footerContainer(
context: context,
child: AppFilledButton(
label: context.translation.confirm,
buttonColor: AppColor.primary10,
onPressed: onConfirm,
),
),
],
),
),
);
}
Widget _buildTaskInfoCard(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoHeader16Widget(
'Task Information',
),
12.height,
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (taskData.oneTimeSetting?.taskTypeName != null) InfoTextWidget(label: 'Task Type', value: taskData.oneTimeSetting!.taskTypeName!),
if (taskData.siteName != null) InfoTextWidget(label: 'Site', value: taskData.siteName!),
if (taskData.buildingName != null) InfoTextWidget(label: 'Building', value: taskData.buildingName!),
if (taskData.floorName != null) InfoTextWidget(label: 'Floor', value: taskData.floorName!),
if (taskData.departmentName != null) InfoTextWidget(label: 'Department', value: taskData.departmentName!),
if (taskData.roomName != null) InfoTextWidget(label: 'Room', value: taskData.roomName!),
if (taskData.locationDescription != null) InfoTextWidget(label: 'Location', value: taskData.locationDescription!),
],
).paddingAll(16),
],
).toShadowContainer(context, padding: 16);
}
Widget _buildWorkingHoursCard(BuildContext context) {
// Parse timer start time from taskData if available
DateTime? timerStartTime;
if (taskData.startTime != null) {
try {
timerStartTime = DateTime.parse(taskData.startTime!);
} catch (e) {
print('Error parsing start time: $e');
}
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoHeader16Widget(
'Working Hours',
),
12.height,
if (timerStartTime != null)
AppTimer(
label: 'Working Time',
timer: TimerModel(
startAt: timerStartTime,
endAt: null,
durationInSecond: 0,
),
enabled: false,
// Disable controls, just show the timer
showTimer: true,
decoration: BoxDecoration(
color: context.isDark ? AppColor.neutral20.withValues(alpha: 0.5) : AppColor.neutral10.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
)
else
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: context.isDark ? AppColor.neutral20.withValues(alpha: 0.5) : AppColor.neutral10.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
InfoTextLabelWidget(
label: 'Total Working Hours',
),
InfoHeader16Widget(
CMRequestUtils.formatTotalWorkingHours(totalWorkingHours),
),
],
),
),
],
).toShadowContainer(context, padding: 16);
}
Widget _buildChecklistCard(BuildContext context) {
final completedInstructions = taskData.instructions?.where((i) => i.isDone == true).toList() ?? [];
final totalInstructions = taskData.instructions?.length ?? 0;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
InfoHeader16Widget(
'Checklist',
),
InfoTextLabelWidget(
label: '${completedInstructions.length}/$totalInstructions',
),
],
),
12.height,
ListView.separated(
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
itemCount: completedInstructions.length,
separatorBuilder: (context, index) => const Divider(height: 1, color: AppColor.neutral30, thickness: 1).paddingOnly(top: 12, bottom: 12),
itemBuilder: (context, index) {
final instruction = completedInstructions[index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.check_circle,
color: AppColor.primary10,
size: 20,
),
8.width,
Expanded(
child: InfoTextLabelWidget(
label: instruction.instructionText ?? '',
),
),
],
),
if (instruction.comment != null && instruction.comment!.isNotEmpty) ...[
8.height,
Container(
width: double.infinity, // Force full width
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: context.isDark ? AppColor.neutral20.withValues(alpha: 0.5) : AppColor.neutral10.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoTextLabelWidget(
label: 'Comment:',
),
4.height,
InfoTextLabelWidget(
label: instruction.comment!,
),
],
),
),
],
],
);
},
).paddingAll(16),
],
).toShadowContainer(context, padding: 16);
}
Widget _buildImagesCard(BuildContext context) {
// Show captured images (base64 from camera)
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoHeader16Widget('Captured Images'),
12.height,
Wrap(
spacing: 8,
runSpacing: 8,
children: taskData.imageRooms!.map((imageString) {
return GestureDetector(
onTap: () {
context.fullScreenImageViewer(_buildImage(imageString));
},
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: context.isDark ? AppColor.neutral20 : AppColor.neutral30,
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: _buildImage(imageString),
),
),
);
}).toList(),
),
],
).toShadowContainer(context, padding: 16);
}
Widget _buildAttachmentsCard(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoHeader16Widget(
'Attachments',
),
FilesList(
images: taskData.attachments!.map((attachment) => URLs.getFileUrl(attachment.name ?? '') ?? '').toList(),
showAsListView: true,
),
],
).toShadowContainer(context, padding: 16);
}
Widget _buildImage(String imageString) {
try {
// imageString format: "filename|base64data"
final parts = imageString.split('|');
final base64Data = parts.length > 1 ? parts[1] : imageString;
return Image.memory(
base64Decode(base64Data),
fit: BoxFit.cover,
);
} catch (e) {
return const Icon(Icons.broken_image, size: 48);
}
}
Widget _buildInfoRow(BuildContext context, String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: InfoTextLabelWidget(
label: '$label:',
),
),
Expanded(
child: InfoTextLabelWidget(
label: value,
),
),
],
),
);
}
}

@ -0,0 +1,444 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/controllers/providers/api/user_provider.dart';
import 'package:test_sa/dashboard_latest/dashboard_provider.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/modules/cleaner_module/models/one_time_task_data.dart';
import 'package:test_sa/modules/cleaner_module/provider/cleaner_provider.dart';
import 'package:test_sa/modules/cleaner_module/widgets/cleaner_checklist_widget.dart';
import 'package:test_sa/modules/cleaner_module/pages/one_time_task_preview_screen.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/new_views/pages/land_page/land_page.dart';
import 'package:test_sa/views/widgets/item_views/info_date_widget.dart';
import 'package:test_sa/views/widgets/item_views/info_header_widget.dart';
import 'package:test_sa/views/widgets/item_views/info_text_widget.dart';
import 'package:test_sa/views/widgets/loaders/app_loading.dart';
import 'package:test_sa/views/widgets/loaders/no_data_found.dart';
import 'package:test_sa/views/widgets/dialogs/confirm_dialog.dart';
import 'package:test_sa/views/widgets/timer/app_timer.dart';
import 'package:test_sa/models/timer_model.dart';
import 'package:test_sa/views/widgets/requests/request_status.dart';
class OneTimeTaskView extends StatefulWidget {
static const String routeName = "/one_time_task";
final int taskId;
final OneTimeTaskData? initialData;
const OneTimeTaskView({Key? key, required this.taskId, this.initialData}) : super(key: key);
@override
State<OneTimeTaskView> createState() => _OneTimeTaskViewState();
}
class _OneTimeTaskViewState extends State<OneTimeTaskView> {
OneTimeTaskData? _taskData;
bool _isLoading = false;
DateTime? _timerStart;
List<ChecklistItem>? _checklistItems; // Cache checklist items
@override
void initState() {
super.initState();
// Use initial data if provided, otherwise load from API
if (widget.initialData != null) {
_taskData = widget.initialData;
// Initialize imageRooms as empty list if null
_taskData?.imageRooms ??= [];
// Start timer automatically if task is in progress
if (_taskData?.statusValue == 1 && _taskData?.startTime != null) {
_timerStart = DateTime.parse(_taskData!.startTime!);
}
} else {
_loadTaskData();
}
}
Future<void> _loadTaskData() async {
setState(() {
_isLoading = true;
});
try {
final result = await CleanerProvider().getOneTimeTaskById(id: widget.taskId);
if (result['success'] && result['data'] != null) {
setState(() {
_taskData = OneTimeTaskData.fromJson(result['data']);
// Initialize imageRooms as empty list if null
_taskData?.imageRooms ??= [];
// Convert attachments from API to imageRooms format for display
if ((_taskData?.imageRooms?.isEmpty ?? true) && (_taskData?.attachments != null && _taskData!.attachments!.isNotEmpty)) {
_taskData!.imageRooms = _taskData!.attachments!.map((attachment) => URLs.getFileUrl(attachment.name ?? '') ?? '').where((url) => url.isNotEmpty).toList();
}
_isLoading = false;
// Start timer automatically if task is in progress
if (_taskData?.statusValue == 1 && _taskData?.startTime != null) {
_timerStart = DateTime.parse(_taskData!.startTime!);
}
});
} else {
setState(() {
_isLoading = false;
});
if (mounted) {
_showErrorDialog(context, result['message'] ?? 'Failed to load task');
}
}
} catch (e) {
setState(() {
_isLoading = false;
});
if (mounted) {
_showErrorDialog(context, 'Failed to load task data');
}
}
}
bool get isClosed => _taskData?.statusValue == 2;
String _formatDuration(Duration duration) {
if (duration.inDays > 0) {
final hours = duration.inHours % 24;
final minutes = duration.inMinutes % 60;
return '${duration.inDays}d ${hours}h ${minutes}m';
} else if (duration.inHours > 0) {
final minutes = duration.inMinutes % 60;
final seconds = duration.inSeconds % 60;
return '${duration.inHours}h ${minutes}m ${seconds}s';
} else if (duration.inMinutes > 0) {
final seconds = duration.inSeconds % 60;
return '${duration.inMinutes}m ${seconds}s';
} else {
return '${duration.inSeconds}s';
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: DefaultAppBar(
title: 'One Time Task'.addTranslation,
onWillPopScope: null,
onBackPress: () {
WidgetsBinding.instance.addPostFrameCallback((_) {
UserProvider userProvider = Provider.of<UserProvider>(context, listen: false);
DashBoardProvider dashBoardProvider = Provider.of<DashBoardProvider>(context, listen: false);
dashBoardProvider.getDashBoardCount(usersType: userProvider.user!.type!);
dashBoardProvider.resetRequestDataList();
dashBoardProvider.getRequestDetail(usersType: userProvider.user!.type!, status: dashBoardProvider.tabs[dashBoardProvider.currentListIndex!].tag);
});
Navigator.pop(context);
},
),
body: _isLoading
? const ALoading()
: _taskData != null
? SafeArea(
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTaskInfoCard(context),
12.height,
CleanerChecklistWidget(
isClosed: isClosed,
checklistItems: _convertInstructionsToChecklist(),
onChecklistChanged: (value) {
setState(() {
_updateInstructions(value);
});
},
capturedImages: _taskData!.imageRooms ?? [],
onImagesChanged: (images) {
setState(() {
_taskData!.imageRooms = images;
});
},
),
],
),
),
),
if (!isClosed) ...[
FooterActionButton.footerContainer(
context: context,
child: AppFilledButton(
label: context.translation.complete,
buttonColor: AppColor.primary10,
onPressed: _onComplete,
),
),
]
],
),
)
: NoDataFound(message: context.translation.noDataFound).center,
).handlePopScope(
cxt: context,
showPopUp: false,
onSave: () {
Navigator.pop(context);
},
);
}
Widget _buildTaskInfoCard(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_taskData?.statusName != null)
StatusLabel(
radius: 4,
label: _taskData!.statusName!,
textColor: AppColor.getHistoryLogStatusTextColorByName(context, _taskData!.statusName!),
backgroundColor: AppColor.getHistoryLogStatusColorByName(context, _taskData!.statusName!),
),
if (_taskData?.createdDate != null) InfoDateWidget(_taskData!.createdDate!.toString()),
],
),
12.height,
if (_taskData?.oneTimeSetting?.taskTypeName != null)
InfoTextLabelWidget(
label: _taskData!.oneTimeSetting!.taskTypeName!,
),
// Text(
// _taskData!.oneTimeSetting!.taskTypeName!,
// style: AppTextStyles.heading4.copyWith(
// color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
// ),
// ),
// 8.height,
if (_taskData?.oneTimeSetting?.instructionDescriptionName != null)
InfoTextLabelWidget(
label: _taskData!.oneTimeSetting!.instructionDescriptionName!,
),
if (_taskData?.siteName != null) InfoTextWidget(label: 'Site', value: _taskData!.siteName!),
if (_taskData?.buildingName != null) InfoTextWidget(label: 'Building', value: _taskData!.buildingName!),
if (_taskData?.floorName != null) InfoTextWidget(label: 'Floor', value: _taskData!.floorName!),
if (_taskData?.departmentName != null) InfoTextWidget(label: 'Department', value: _taskData!.departmentName!),
if (_taskData?.roomName != null) InfoTextWidget(label: 'Room', value: _taskData!.roomName!),
if (_taskData?.locationDescription != null) InfoTextWidget(label: 'Location', value: _taskData!.locationDescription!),
if (_timerStart != null && !isClosed) ...[
12.height,
AppTimer(
label: 'Working Time',
timer: TimerModel(
startAt: _timerStart,
endAt: null,
durationInSecond: 0,
),
enabled: false,
// Disable controls, just show the timer
showTimer: true,
decoration: BoxDecoration(
color: context.isDark ? AppColor.neutral20.withValues(alpha: 0.5) : AppColor.neutral10.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
),
],
// Display total duration if task is completed
if (isClosed && _taskData?.startTime != null && _taskData?.endTime != null) ...[
// 12.height,
Text(
'Total Duration: ${_formatDuration(DateTime.parse(_taskData!.endTime!).difference(DateTime.parse(_taskData!.startTime!)))}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
],
],
),
],
).toShadowContainer(context);
}
List<ChecklistItem> _convertInstructionsToChecklist() {
// If checklist items are already created, reuse them
if (_checklistItems != null) {
// Update the existing items with latest data from instructions
for (var item in _checklistItems!) {
final instruction = _taskData?.instructions?.firstWhere((i) => i.id == item.id, orElse: () => Instruction());
if (instruction?.id != null) {
item.isChecked = instruction!.isDone ?? false;
// Only update comment if it's different to avoid cursor jumping
if (item.comment != (instruction.comment ?? '')) {
item.comment = instruction.comment ?? '';
}
}
}
return _checklistItems!;
}
// Create new checklist items only on first call
_checklistItems = _taskData?.instructions?.map((instruction) {
return ChecklistItem(
id: instruction.id ?? 0,
title: instruction.instructionText ?? '',
isChecked: instruction.isDone ?? false,
isMandatory: instruction.isRequired ?? false,
comment: instruction.comment ?? '',
);
}).toList() ??
[];
return _checklistItems!;
}
void _updateInstructions(List<ChecklistItem> checklist) {
_taskData?.instructions = checklist.map((item) {
return Instruction(
id: item.id,
instructionTextId: _taskData?.instructions?.firstWhere((i) => i.id == item.id).instructionTextId,
instructionText: item.title,
isDone: item.isChecked,
isRequired: item.isMandatory,
comment: item.comment,
);
}).toList();
}
void _onComplete() {
// Validate mandatory checklist items
final uncheckedMandatory = _taskData?.instructions?.where((item) => (item.isRequired ?? false) && !(item.isDone ?? false)).toList() ?? [];
if (uncheckedMandatory.isNotEmpty) {
_showErrorDialog(context, "Please complete all mandatory tasks (marked with *)");
return;
}
// Validate that at least one image is captured
final capturedImages = _taskData?.imageRooms ?? [];
if (capturedImages.isEmpty) {
_showErrorDialog(context, "Please capture at least one image before completing");
return;
}
double totalWorkingHours = 0;
if (_timerStart != null) {
final duration = DateTime.now().difference(_timerStart!);
totalWorkingHours = duration.inSeconds / 3600.0;
}
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => OneTimeTaskPreviewScreen(
taskData: _taskData!,
totalWorkingHours: totalWorkingHours,
onConfirm: () {
Navigator.pop(context); // Close preview screen
_performComplete(); // Proceed with completion
},
),
),
);
}
Future<void> _performComplete() async {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const AppLazyLoading(),
);
try {
final instructionsPayload = _taskData?.instructions?.map((instruction) {
return {
'id': instruction.id,
'isDone': instruction.isDone,
'comment': instruction.comment,
};
}).toList() ??
[];
final attachmentsPayload = _taskData?.imageRooms?.map((imageString) {
// imageString format: "filename|base64data"
final parts = imageString.split('|');
final originalName = parts.isNotEmpty ? parts[0] : 'image.jpg';
final base64Data = parts.length > 1 ? parts[1] : imageString;
return {
'id': 0,
'name': "$originalName|$base64Data", // Send full formatted string
'originalName': originalName,
};
}).toList() ??
[];
final result = await CleanerProvider().completeOneTimeTask(
id: _taskData!.id!,
instructions: instructionsPayload,
attachments: attachmentsPayload,
);
if (mounted) Navigator.pop(context); // Close loading dialog
if (result['success']) {
if (mounted) {
// Navigate to dashboard
Navigator.of(context).pushNamedAndRemoveUntil(
LandPage.routeName,
(routes) => false,
);
}
} else {
if (mounted) {
_showErrorDialog(context, result['message'] ?? 'Failed to complete task');
}
}
} catch (e) {
if (mounted) Navigator.pop(context);
if (mounted) {
_showErrorDialog(context, 'Failed to complete task. Please try again.');
}
}
}
void _showErrorDialog(BuildContext context, String message) {
showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: message,
title: 'Error',
onTap: () => Navigator.pop(context),
),
);
}
@override
void dispose() {
// Dispose all checklist item controllers
if (_checklistItems != null) {
for (var item in _checklistItems!) {
item.dispose();
}
}
super.dispose();
}
}

@ -102,6 +102,7 @@ class CleanerProvider extends ChangeNotifier {
int? floorId,
int? departmentId,
int? roomId,
int? taskTypeId,
}) async {
Response response;
try {
@ -115,6 +116,7 @@ class CleanerProvider extends ChangeNotifier {
"floorId": floorId,
"departmentId": departmentId,
"roomId": roomId,
"taskTypeId": taskTypeId,
},
);
@ -145,6 +147,7 @@ class CleanerProvider extends ChangeNotifier {
"planRecurrentTaskId": planRecurrentTaskId,
"qrCode": qrCode,
},
showToast: false, // Disable automatic toast message
);
if (response.statusCode >= 200 && response.statusCode < 300) {
@ -159,6 +162,227 @@ class CleanerProvider extends ChangeNotifier {
}
}
/// Check if QR code matches the same room for a plan recurrent task
/// Returns a map with success status and message from backend
Future<Map<String, dynamic>> checkQRSameRoomWithMessage({
required int planRecurrentTaskId,
required String qrCode,
}) async {
Response response;
try {
response = await ApiManager.instance.post(
URLs.checkQRSameRoom,
body: {
"planRecurrentTaskId": planRecurrentTaskId,
"qrCode": qrCode,
},
showToast: false, // Disable automatic toast message
);
// Parse the response body to extract the message
final responseData = json.decode(response.body);
if (response.statusCode >= 200 && response.statusCode < 300) {
// Success response
final isValid = responseData["data"] == true || responseData["isSuccess"] == true;
return {
"success": isValid,
"message": responseData["message"] ?? (isValid ? "QR code validated successfully" : "QR code validation failed"),
};
} else {
// Error response - extract the exact error message from backend
return {
"success": false,
"message": responseData["message"] ?? "QR code validation failed",
};
}
} catch (error) {
debugPrint('Error checking QR same room: $error');
return {
"success": false,
"message": "Failed to validate QR code. Please check your connection and try again.",
};
}
}
/// Scan QR Code and get task details
/// Returns a map with success status, message, and data from backend
Future<Map<String, dynamic>> scanQRCode({
required String qrCode,
}) async {
Response response;
try {
response = await ApiManager.instance.post(
URLs.scanQRCode,
body: {
"qrCode": qrCode,
},
showToast: false, // Disable automatic toast message
);
// Parse the response body to extract the message
final responseData = json.decode(response.body);
if (response.statusCode >= 200 && response.statusCode < 300) {
// Success response
return {
"success": true,
"data": responseData["data"],
"message": responseData["message"] ?? "QR code scanned successfully",
};
} else {
// Error response - extract the exact error message from backend
return {
"success": false,
"data": null,
"message": responseData["message"] ?? "QR code scan failed",
};
}
} catch (error) {
debugPrint('Error scanning QR code: $error');
return {
"success": false,
"data": null,
"message": "Failed to scan QR code. Please check your connection and try again.",
};
}
}
/// Proceed with OneTime Task request
/// Returns a map with success status, message, and data from backend
Future<Map<String, dynamic>> proceedOneTimeTask({
required int oneTimeSettingId,
required int siteId,
required int buildingId,
required int floorId,
required int departmentId,
required int roomId,
}) async {
Response response;
try {
response = await ApiManager.instance.post(
URLs.proceedRequest,
body: {
"oneTimeSettingId": oneTimeSettingId,
"siteId": siteId,
"buildingId": buildingId,
"floorId": floorId,
"departmentId": departmentId,
"roomId": roomId,
},
showToast: false, // Disable automatic toast message
);
// Parse the response body to extract the message
final responseData = json.decode(response.body);
if (response.statusCode >= 200 && response.statusCode < 300) {
// Success response
return {
"success": true,
"data": responseData["data"],
"message": responseData["message"] ?? "Request proceeded successfully",
};
} else {
// Error response - extract the exact error message from backend
return {
"success": false,
"data": null,
"message": responseData["message"] ?? "Failed to proceed with request",
};
}
} catch (error) {
debugPrint('Error proceeding with request: $error');
return {
"success": false,
"data": null,
"message": "Failed to proceed with request. Please check your connection and try again.",
};
}
}
/// Get OneTime Task by ID using getRequestById endpoint
Future<Map<String, dynamic>> getOneTimeTaskById({
required int id,
}) async {
Response response;
try {
response = await ApiManager.instance.get(
'${URLs.getRequestById}?id=$id',
);
// Parse the response body
final responseData = json.decode(response.body);
if (response.statusCode >= 200 && response.statusCode < 300) {
// Success response
return {
"success": true,
"data": responseData["data"],
"message": responseData["message"] ?? "Task retrieved successfully",
};
} else {
// Error response
return {
"success": false,
"data": null,
"message": responseData["message"] ?? "Failed to retrieve task",
};
}
} catch (error) {
debugPrint('Error getting one time task: $error');
return {
"success": false,
"data": null,
"message": "Failed to retrieve task. Please check your connection and try again.",
};
}
}
/// Complete OneTime Task
Future<Map<String, dynamic>> completeOneTimeTask({
required int id,
required List<Map<String, dynamic>> instructions,
List<Map<String, dynamic>>? attachments,
}) async {
Response response;
try {
response = await ApiManager.instance.post(
URLs.completeRequest,
body: {
"id": id,
"instructions": instructions,
"attachments": attachments ?? [],
},
showToast: false,
);
final responseData = json.decode(response.body);
if (response.statusCode >= 200 && response.statusCode < 300) {
return {
"success": true,
"data": responseData["data"],
"message": responseData["message"] ?? "Task completed successfully",
};
} else {
return {
"success": false,
"data": null,
"message": responseData["message"] ?? "Failed to complete task",
};
}
} catch (error) {
debugPrint('Error completing one time task: $error');
return {
"success": false,
"data": null,
"message": "Failed to complete task. Please check your connection and try again.",
};
}
}
void reset() {
_isLoading = false;
_searchResults = null;

@ -0,0 +1,74 @@
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:http/http.dart';
import 'package:test_sa/controllers/api_routes/api_manager.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/modules/cleaner_module/models/task_type.dart';
class TaskTypeProvider extends ChangeNotifier {
//reset provider data
void reset() {
_taskTypes = null;
_stateCode = null;
}
// state code of current request to define error message
int? _stateCode;
int? get stateCode => _stateCode;
// contain task types data
List<TaskType>? _taskTypes;
List<TaskType>? get taskTypes => _taskTypes;
// loading state
bool _loading = false;
bool get isLoading => _loading;
set isLoading(bool isLoading) {
_loading = isLoading;
notifyListeners();
}
/// Get task types
Future<int> getTaskTypes() async {
if (_loading == true) return -2;
_loading = true;
notifyListeners();
Response response;
try {
response = await ApiManager.instance.get(URLs.getTaskType);
_stateCode = response.statusCode;
if (response.statusCode >= 200 && response.statusCode < 300) {
// client's request was successfully received
List taskTypesListJson = json.decode(response.body)["data"];
_taskTypes = taskTypesListJson.map((taskType) => TaskType.fromJson(taskType)).toList();
}
_loading = false;
notifyListeners();
return response.statusCode;
} catch (error) {
_loading = false;
_stateCode = -1;
notifyListeners();
return -1;
}
}
/// Get task types list directly without provider state
Future<List<TaskType>> getTaskTypesList() async {
Response response;
try {
response = await ApiManager.instance.get(URLs.getTaskType);
if (response.statusCode >= 200 && response.statusCode < 300) {
List taskTypesListJson = json.decode(response.body)["data"];
return taskTypesListJson.map((taskType) => TaskType.fromJson(taskType)).toList();
}
return [];
} catch (error) {
return [];
}
}
}

@ -1,10 +1,6 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:image_picker/image_picker.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart';
@ -13,15 +9,19 @@ import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/app_text_form_field.dart';
import 'package:test_sa/views/widgets/buttons/app_back_button.dart';
import 'package:test_sa/modules/cleaner_module/widgets/multi_image_picker.dart';
import 'package:test_sa/views/widgets/images/files_list.dart';
import 'package:test_sa/views/widgets/item_views/info_header_widget.dart';
import 'package:test_sa/views/widgets/item_views/info_text_widget.dart';
class CleanerChecklistWidget extends StatefulWidget {
final List<ChecklistItem> checklistItems;
final Function(List<ChecklistItem>)? onChecklistChanged;
final String? capturedImageBase64;
final Function(String?)? onImageCaptured;
bool isClosed;
final List<String> capturedImages;
final Function(List<String>)? onImagesChanged;
final bool isClosed;
CleanerChecklistWidget({Key? key, required this.checklistItems, this.onChecklistChanged, this.capturedImageBase64, this.onImageCaptured, this.isClosed = false}) : super(key: key);
const CleanerChecklistWidget({Key? key, required this.checklistItems, this.onChecklistChanged, required this.capturedImages, this.onImagesChanged, this.isClosed = false}) : super(key: key);
@override
State<CleanerChecklistWidget> createState() => _CleanerChecklistWidgetState();
@ -37,16 +37,17 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 12,
children: [
Text(
'Checklist'.addTranslation,
style: AppTextStyles.heading4.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
),
),
16.height,
...widget.checklistItems.map((item) => _buildChecklistItem(context, item)).toList(),
// 16.height,
InfoHeader16Widget('Checklist'),
ListView.separated(
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (cxt, index) => _buildChecklistItem(context, widget.checklistItems[index]),
separatorBuilder: (cxt, index) => const Divider().defaultStyle(context),
itemCount: widget.checklistItems.length)
.toShadowContainer(context),
_buildImageCaptureWidget(context),
],
);
@ -59,28 +60,68 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Checkbox with title
Row(
children: [
Checkbox(
// visualDensity: const VisualDensity(horizontal: -4.0, vertical: -4.0),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
AbsorbPointer(
absorbing: true,
child: Transform.scale(
scale: 1.2,
child: Checkbox(
value: item.isChecked,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
side: BorderSide(color: AppColor.loadingColor(context), width: 1.0),
visualDensity: const VisualDensity(horizontal: -4.0, vertical: -4.0),
activeColor: AppColor.primary10,
onChanged: (bool? value) {},
),
),
),
10.width,
RichText(
text: TextSpan(
children: [
TextSpan(
text: item.title,
style: AppTextStyles.bodyText2.copyWith(color: AppColor.lightTextColor(context)),
),
if (item.isMandatory)
TextSpan(
text: ' *',
style: AppTextStyles.bodyText.copyWith(
color: Colors.red,
fontWeight: FontWeight.w500,
),
),
],
),
).expanded,
],
).onPress(() {
setState(() {
item.isChecked = !item.isChecked;
if (!item.isChecked) {
item.commentController.clear();
}
});
widget.onChecklistChanged?.call(widget.checklistItems);
}),
/* ListTileTheme(
horizontalTitleGap: 0,
contentPadding: EdgeInsets.zero,
minVerticalPadding: 0,
child: Container(
color: Colors.amberAccent,
child: CheckboxListTile(
value: item.isChecked,
minVerticalPadding: 0,
controlAffinity: ListTileControlAffinity.leading,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
contentPadding: EdgeInsets.zero,
checkboxScaleFactor: 1.2,
side: BorderSide(color: Colors.black.withValues(alpha: .05)),
visualDensity: const VisualDensity(horizontal: -0.0, vertical: -4.0),
activeColor: AppColor.primary10,
onChanged: (bool? value) {
setState(() {
item.isChecked = value ?? false;
if (!item.isChecked) {
item.commentController.clear();
}
});
// Notify parent of changes
widget.onChecklistChanged?.call(widget.checklistItems);
},
),
8.width,
Expanded(
child: RichText(
title: RichText(
text: TextSpan(
children: [
TextSpan(
@ -101,32 +142,36 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
],
),
),
onChanged: (bool? value) {
setState(() {
item.isChecked = value ?? false;
if (!item.isChecked) {
item.commentController.clear();
}
});
widget.onChecklistChanged?.call(widget.checklistItems);
},
),
],
),
// Comment box - only visible when checked
),
),*/
if (item.isChecked) ...[
16.height,
12.height,
AppTextFormField(
controller: item.commentController,
labelText: context.translation.comment,
backgroundColor: AppColor.fieldBgColor(context),
showShadow: false,
labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(
color: context.isDark ? AppColor.white10 : AppColor.black10,
),
labelStyle: AppTextStyles.textFieldLabelStyle,
alignLabelWithHint: true,
textInputType: TextInputType.multiline,
onChange: (value) {
item.comment = value;
// Notify parent of changes
widget.onChecklistChanged?.call(widget.checklistItems);
},
),
],
],
).toShadowContainer(context).paddingOnly(bottom: 16),
),
);
}
@ -135,217 +180,142 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Captured Image'.addTranslation,
style: AppTextStyles.heading6.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
),
),
InfoHeader16Widget('Captured Image'),
8.height,
_buildImageWidget(context),
],
).toShadowContainer(context);
}
static const int maxImages = 2;
/// Build the image widget with loading indicator for network images
Widget _buildImageWidget(BuildContext context) {
if (widget.capturedImageBase64 == null || widget.capturedImageBase64!.isEmpty) {
// Show camera icon when no image
return Container(
height: 200,
width: double.infinity,
decoration: BoxDecoration(
color: AppColor.fieldBgColor(context),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.camera_alt,
size: 48,
color: context.isDark ? AppColor.white10 : AppColor.black10,
),
).onPress(() async {
final pickedFile = await ImagePicker().pickImage(source: ImageSource.camera, imageQuality: 70, maxWidth: 800, maxHeight: 800);
if (pickedFile != null) {
// Convert image to Base64 with filename prefix (same format as CM request)
final bytes = await pickedFile.readAsBytes();
final base64Image = base64Encode(bytes);
final fileName = pickedFile.path.split("/").last;
final formattedImage = "$fileName|$base64Image";
if (widget.capturedImages.isEmpty) {
// Show add image button when no image
return GestureDetector(
onTap: widget.isClosed
? null
: () async {
// Only allow camera - pick one image at a time
final pickedFile = await ImagePicker().pickImage(
source: ImageSource.camera,
imageQuality: 70,
maxWidth: 800,
maxHeight: 800,
);
// Update parent with captured image
widget.onImageCaptured?.call(formattedImage);
}
});
}
if (pickedFile != null) {
final bytes = await pickedFile.readAsBytes();
final base64Image = base64Encode(bytes);
final fileName = pickedFile.path.split("/").last;
final formattedImage = "$fileName|$base64Image";
Widget imageWidget;
// Add to existing images (max 2)
final updatedImages = List<String>.from(widget.capturedImages);
updatedImages.add(formattedImage);
final imageString = widget.capturedImageBase64!;
widget.onImagesChanged?.call(updatedImages);
}
},
child: Container(
height: 120,
width: 120,
decoration: BoxDecoration(
color: AppColor.fieldBgColor(context),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: context.isDark ? AppColor.neutral20 : AppColor.neutral30,
width: 2,
style: BorderStyle.solid,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.add_photo_alternate,
size: 32,
color: context.isDark ? AppColor.white10 : AppColor.black10,
),
4.height,
InfoTextLabelWidget(label: 'Add Image'),
// First check if it's base64 encoded (contains | separator)
if (imageString.contains('|')) {
// Format: "filename|base64data"
try {
final base64Data = imageString.split('|').last;
imageWidget = Image.memory(base64Decode(base64Data), fit: BoxFit.cover);
return Stack(
alignment: AlignmentDirectional.topEnd,
children: [
Container(
height: 200,
width: double.infinity,
decoration: BoxDecoration(
color: AppColor.fieldBgColor(context),
borderRadius: BorderRadius.circular(8),
),
child: imageWidget)
.onPress(() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => Scaffold(
body: SafeArea(
child: Stack(
children: [
// InteractiveViewer(child: Image(image: getImageObject(itemIndex))).center,
InteractiveViewer(child: imageWidget).center,
const ABackButton(),
],
),
),
),
),
);
}),
_delete(),
],
);
} catch (e) {
debugPrint('Error decoding base64 image: $e');
return _buildCameraIcon(context);
}
// Text(
// 'Add Image',
// style: AppTextStyles.tinyFont.copyWith(
// color: context.isDark ? AppColor.white10 : AppColor.black10,
// ),
// ),
],
),
),
);
}
// Check if it's a local file path
if (_isLocalUrl(imageString)) {
try {
imageWidget = Image.file(File(imageString), fit: BoxFit.cover);
return Stack(
alignment: AlignmentDirectional.topEnd,
children: [
Container(
height: 200,
width: double.infinity,
decoration: BoxDecoration(
color: AppColor.fieldBgColor(context),
borderRadius: BorderRadius.circular(8),
),
child: imageWidget)
.onPress(() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => Scaffold(
body: SafeArea(
child: Stack(
children: [
// InteractiveViewer(child: Image(image: getImageObject(itemIndex))).center,
InteractiveViewer(child: imageWidget).center,
const ABackButton(),
],
),
),
),
),
);
}),
_delete(),
],
);
} catch (e) {
debugPrint('Error loading local file: $e');
return _buildCameraIcon(context);
}
}
// Check if images are URLs from API (like ServiceRequestDetailView does)
final bool hasApiImages = widget.capturedImages.any((img) => img.startsWith('http://') || img.startsWith('https://'));
// If not local and not base64, it's a server filename - use NetworkImage with loading indicator
final imageUrl = URLs.getFileUrl(imageString);
if (imageUrl != null) {
imageWidget = Image.network(
imageUrl,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) {
return child;
}
return Center(
child: CircularProgressIndicator(
color: AppColor.primary10,
value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null,
),
);
},
errorBuilder: (context, error, stackTrace) {
debugPrint('Error loading network image: $error');
return _buildCameraIcon(context);
},
);
return Stack(
alignment: AlignmentDirectional.topEnd,
children: [
Container(
height: 200,
width: double.infinity,
decoration: BoxDecoration(
color: AppColor.fieldBgColor(context),
borderRadius: BorderRadius.circular(8),
),
child: imageWidget)
.onPress(() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => Scaffold(
body: SafeArea(
child: Stack(
children: [
// InteractiveViewer(child: Image(image: getImageObject(itemIndex))).center,
InteractiveViewer(child: imageWidget).center,
const ABackButton(),
],
),
),
),
),
);
}),
_delete(),
],
// If images are from API (URLs), use FilesList widget like ServiceRequestDetailView
if (hasApiImages && widget.isClosed) {
return FilesList(
images: widget.capturedImages,
showAsListView: false,
);
}
return _buildCameraIcon(context);
}
/// Build camera icon widget
Widget _buildCameraIcon(BuildContext context) {
return Container(
height: 200,
width: double.infinity,
decoration: BoxDecoration(
color: AppColor.fieldBgColor(context),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.camera_alt,
size: 48,
color: context.isDark ? AppColor.white10 : AppColor.black10,
),
// Otherwise, use MultiImagePicker for base64 images or when editing
return MultiImagePicker(
imageUrls: widget.capturedImages,
enabled: !widget.isClosed,
maxImages: 2,
// Enforce max 2 images
onImageTap: (index, imageUrl) {
context.fullScreenImageViewer(_buildImageForViewing(imageUrl));
},
onDelete: (index) {
// Handle image delete
if (index >= 0) {
setState(() {
widget.capturedImages.removeAt(index);
});
widget.onImagesChanged?.call(widget.capturedImages);
}
},
onAdd: (imageUrl) {
// Handle new image addition
setState(() {
widget.capturedImages.add(imageUrl);
});
widget.onImagesChanged?.call(widget.capturedImages);
},
);
}
/// Check if the URL is a local file path
/// Same logic as multi_image_picker_item.dart
bool _isLocalUrl(String url) {
if (url.isEmpty) return false;
return url.startsWith("/") || url.startsWith("file://") || (url.length > 1 && url.substring(1).startsWith(':\\'));
/// Build image widget for viewing based on source type
Widget _buildImageForViewing(String imageUrl) {
try {
// Check if it's a URL from API
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
return Image.network(
imageUrl,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return const Icon(Icons.broken_image, size: 48, color: Colors.white);
},
);
}
// Otherwise, it's base64 encoded
final parts = imageUrl.split('|');
final base64Data = parts.length > 1 ? parts[1] : imageUrl;
return Image.memory(
base64Decode(base64Data),
fit: BoxFit.contain,
);
} catch (e) {
return const Icon(Icons.broken_image, size: 48, color: Colors.white);
}
}
@override
@ -353,26 +323,6 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
// Note: Don't dispose controllers here as they're managed by ChecklistItem
super.dispose();
}
Widget _delete() {
if (widget.isClosed) return const SizedBox();
return Container(
height: 45.toScreenHeight,
width: 45.toScreenHeight,
margin: const EdgeInsets.only(top: 4, right: 4),
decoration: BoxDecoration(
color: (context.isDark ? AppColor.neutral20 : AppColor.neutral40).withValues(alpha: 0.4),
border: Border.all(
color: (context.isDark ? AppColor.neutral20 : AppColor.neutral40).withValues(alpha: 0.7),
),
borderRadius: BorderRadius.circular(30),
//boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10)],
),
child: "delete".toSvgAsset())
.onPress(() {
widget.onImageCaptured?.call(null);
});
}
}
/// Model class for checklist items
@ -390,10 +340,7 @@ class ChecklistItem {
required this.isChecked,
required this.comment,
this.isMandatory = false,
}) : commentController = TextEditingController(text: comment) {
// Update the controller text whenever comment changes
commentController.text = comment;
}
}) : commentController = TextEditingController(text: comment);
// Convert to JSON for API submission
Map<String, dynamic> toJson() {

@ -0,0 +1,197 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/views/widgets/buttons/app_back_button.dart';
import 'package:test_sa/modules/cm_module/cm_request_utils.dart';
class MultiImagePicker extends StatelessWidget {
final List<String> imageUrls;
final Function(int index, String imageUrl)? onImageTap;
final Function(int index)? onDelete;
final Function(String imageUrl)? onAdd;
final bool enabled;
final int maxImages;
const MultiImagePicker({
Key? key,
required this.imageUrls,
this.onImageTap,
this.onDelete,
this.onAdd,
this.enabled = true,
this.maxImages = 2,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Display existing images horizontally with add button
Row(
children: [
// Display existing images
...List.generate(
imageUrls.length,
(index) {
return Padding(
padding: EdgeInsets.only(right: 8.toScreenWidth),
child: _buildImageItem(context, index),
);
},
),
// Add image button (only show if under max limit)
if (enabled && imageUrls.length < maxImages) _buildAddImageButton(context),
],
),
],
);
}
Widget _buildImageItem(BuildContext context, int index) {
final imageUrl = imageUrls[index];
return Stack(
alignment: AlignmentDirectional.topEnd,
children: [
Container(
height: 120,
width: 120,
decoration: BoxDecoration(
color: AppColor.fieldBgColor(context),
borderRadius: BorderRadius.circular(8),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: _getImageWidget(imageUrl),
),
).onPress(() {
if (onImageTap != null) {
onImageTap!(index, imageUrl);
} else {
context.fullScreenImageViewer(
_getImageWidget(imageUrl),
);
}
}),
if (enabled)
Container(
height: 32,
width: 32,
margin: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: (context.isDark ? AppColor.neutral20 : AppColor.neutral40).withValues(alpha: 0.9),
border: Border.all(
color: (context.isDark ? AppColor.neutral20 : AppColor.neutral40).withValues(alpha: 0.7),
),
borderRadius: BorderRadius.circular(20),
),
child: const Icon(Icons.close, color: Color(0xffF63939), size: 20),
).onPress(() {
if (onDelete != null) {
onDelete!(index);
}
}),
],
);
}
Widget _buildAddImageButton(BuildContext context) {
return GestureDetector(
onTap: () => _pickImage(context),
child: Container(
height: 120,
width: 120,
decoration: BoxDecoration(
color: AppColor.fieldBgColor(context),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: context.isDark ? AppColor.neutral20 : AppColor.neutral30,
width: 2,
style: BorderStyle.solid,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.add_photo_alternate,
size: 32,
color: context.isDark ? AppColor.white10 : AppColor.black10,
),
4.height,
Text(
'Add Image',
style: AppTextStyles.tinyFont.copyWith(
color: context.isDark ? AppColor.white10 : AppColor.black10,
),
),
],
),
),
);
}
Widget _getImageWidget(String imageUrl) {
// Check if it's base64 encoded (contains | separator)
if (imageUrl.contains('|')) {
try {
final base64Data = imageUrl.split('|').last;
return Image.memory(
base64Decode(base64Data),
fit: BoxFit.cover,
);
} catch (e) {
return const Icon(Icons.broken_image, size: 48);
}
}
// Check if it's a local file path
if (CMRequestUtils.isLocalUrl(imageUrl)) {
return Image.file(
File(imageUrl),
fit: BoxFit.cover,
);
}
// Otherwise treat as network URL
return Image.network(
imageUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return const Icon(Icons.broken_image, size: 48);
},
);
}
void _pickImage(BuildContext context) async {
// Directly open camera - no bottom sheet
final ImagePicker picker = ImagePicker();
final XFile? pickedFile = await picker.pickImage(
source: ImageSource.camera,
imageQuality: 70,
maxWidth: 800,
maxHeight: 800,
);
if (pickedFile != null) {
final bytes = await pickedFile.readAsBytes();
final base64Image = base64Encode(bytes);
final fileName = pickedFile.path.split("/").last;
final formattedImage = "$fileName|$base64Image";
// Call the onAdd callback to notify parent
if (onAdd != null) {
onAdd!(formattedImage);
}
}
}
}

@ -0,0 +1,112 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/all_requests_and_count_model.dart';
import 'package:test_sa/models/new_models/dashboard_detail.dart';
import 'package:test_sa/modules/cleaner_module/pages/one_time_task_view.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/views/widgets/item_views/info_date_widget.dart';
import 'package:test_sa/views/widgets/item_views/info_header_widget.dart';
import 'package:test_sa/views/widgets/item_views/info_text_widget.dart';
import 'package:test_sa/views/widgets/requests/request_status.dart';
class OneTimeTaskItemView extends StatelessWidget {
final Data? requestData;
final RequestsDetails? requestDetails;
final bool showShadow;
const OneTimeTaskItemView({Key? key, this.requestData, this.requestDetails, this.showShadow = true}) : super(key: key);
@override
Widget build(BuildContext context) {
if (requestData != null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
StatusLabel(
label: requestData!.statusName ?? '',
textColor: AppColor.getRequestStatusTextColorByName(context, requestData!.statusName ?? ''),
backgroundColor: AppColor.getRequestStatusColorByName(context, requestData!.statusName ?? ''),
),
1.width.expanded,
InfoDateWidget(requestData!.transactionDate ?? ""),
],
),
8.height,
InfoHeaderWidget(requestData?.typeTransaction ?? 'One Time Task'.addTranslation),
InfoTextWidget(label: context.translation.taskNo, value: requestData!.requestNo),
if (requestData?.siteName != null) InfoTextWidget(label: context.translation.site, value: requestData!.siteName),
if (requestData!.buildingName != null) InfoTextWidget(label: context.translation.building, value: requestData!.buildingName),
if (requestData!.departmentName != null) InfoTextWidget(label: context.translation.department, value: requestData!.departmentName),
if (requestData!.floorName != null) InfoTextWidget(label: context.translation.floor, value: requestData!.floorName),
InfoTextWidget(
label: context.translation.room,
value: requestData!.roomName,
showArrow: true,
),
],
).toShadowContainer(context, withShadow: showShadow).onPress(() {
// Navigate to editable view for in-progress tasks
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => OneTimeTaskView(
taskId: requestData!.id ?? 0,
initialData: null,
),
),
);
});
}
if (requestDetails != null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
StatusLabel(
label: requestDetails!.status!,
textColor: AppColor.getRequestStatusTextColorByName(context, requestDetails!.status!),
backgroundColor: AppColor.getRequestStatusColorByName(context, requestDetails!.status!),
),
1.width.expanded,
InfoDateWidget(requestDetails!.date ?? ""),
],
),
8.height,
InfoHeaderWidget(requestDetails?.nameOfType ?? 'One Time Task'.addTranslation),
InfoTextWidget(label: context.translation.taskNo, value: requestDetails!.requestNo),
if (requestDetails!.siteName != null) InfoTextWidget(label: context.translation.site, value: requestDetails!.siteName),
if (requestDetails!.buildingName != null) InfoTextWidget(label: context.translation.building, value: requestDetails!.buildingName),
if (requestDetails!.departmentName != null) InfoTextWidget(label: context.translation.department, value: requestDetails!.departmentName),
if (requestDetails!.floorName != null) InfoTextWidget(label: context.translation.floor, value: requestDetails!.floorName),
InfoTextWidget(
label: context.translation.room,
value: requestDetails!.roomName,
showArrow: true,
),
],
).toShadowContainer(context, withShadow: showShadow).onPress(() async {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => OneTimeTaskView(
taskId: requestDetails!.id ?? 0,
initialData: null,
),
),
);
});
}
return const SizedBox();
}
}

@ -89,24 +89,25 @@ class _RecurrentWorkOrderViewState extends State<RecurrentWorkOrderView> {
)
: const SizedBox(),
),
16.height,
requestProvider.recurrentWoData!.planRecurrentTaskCheckLists != null && requestProvider.recurrentWoData!.planRecurrentTaskCheckLists!.isNotEmpty
? CleanerChecklistWidget(
isClosed: isClosed,
checklistItems: requestProvider.recurrentWoData!.planRecurrentTaskCheckLists ?? [],
onChecklistChanged: (value) {
setState(() {
requestProvider.recurrentWoData!.planRecurrentTaskCheckLists = value;
});
},
capturedImageBase64: requestProvider.recurrentWoData!.imageRoom,
onImageCaptured: (imageBase64) {
setState(() {
requestProvider.recurrentWoData!.imageRoom = imageBase64;
});
},
)
: const SizedBox(),
//TODO: need to check this
// 16.height,
// requestProvider.recurrentWoData!.planRecurrentTaskCheckLists != null && requestProvider.recurrentWoData!.planRecurrentTaskCheckLists!.isNotEmpty
// ? CleanerChecklistWidget(
// isClosed: isClosed,
// checklistItems: requestProvider.recurrentWoData!.planRecurrentTaskCheckLists ?? [],
// onChecklistChanged: (value) {
// setState(() {
// requestProvider.recurrentWoData!.planRecurrentTaskCheckLists = value;
// });
// },
// capturedImageBase64: requestProvider.recurrentWoData!.imageRoom,
// onImageCaptured: (imageBase64) {
// setState(() {
// requestProvider.recurrentWoData!.imageRoom = imageBase64;
// });
// },
// )
// : const SizedBox(),
],
),
).expanded,

@ -19,12 +19,13 @@ class AppBottomNavigationBar extends StatelessWidget {
bool isEngineer = (Provider.of<UserProvider>(context, listen: false).user?.type) == UsersTypes.engineer;
bool isUserFMS = Provider.of<SettingProvider>(context, listen: false).isUserFMS;
bool commissioningEng = context.userProvider.isCommissioningEngineer;
bool isHouseKeeper = context.userProvider.isHouseKeeper;
List<NavItemModel> navItems = [
NavItemModel(0, "overview", context.translation.overview),
NavItemModel(1, "request_icon", context.translation.workOrder),
if (!commissioningEng) NavItemModel(2, "add_icon", context.translation.calendar, showLabel: false),
NavItemModel(3, "assets", context.translation.assets),
if(!isHouseKeeper) NavItemModel(1, "request_icon", context.translation.workOrder),
if (!commissioningEng&& !isHouseKeeper) NavItemModel(2, "add_icon", context.translation.calendar, showLabel: false),
if(!isHouseKeeper) NavItemModel(3, "assets", context.translation.assets),
NavItemModel(4, "contact", context.translation.contact),
];

@ -115,7 +115,9 @@ class _LandPageState extends State<LandPage> {
WidgetsBinding.instance.addPostFrameCallback((_) {
_userProvider!.getSwipeLastTransaction(userId: _userProvider!.user!.userID!);
_userProvider!.getSiteContactInfo();
Provider.of<VendorProvider>(context, listen: false).getData();
if(!_userProvider!.isHouseKeeper) {
Provider.of<VendorProvider>(context, listen: false).getData();
}
});
}
_pages = <Widget>[

@ -144,7 +144,11 @@ class _AllRequestsFilterPageState extends State<AllRequestsFilterPage> {
types['Incidents'] = 16;
}
final statuses = {
if (context.userProvider.isHouseKeeper) {
types = {context.translation.recurrentWo: 5};
}
var statuses = {
"All WO": 0,
context.translation.open: 1,
context.translation.inProgress: 2,
@ -152,6 +156,16 @@ class _AllRequestsFilterPageState extends State<AllRequestsFilterPage> {
context.translation.closed: 3,
"Canceled": 5,
};
if (context.userProvider.isHouseKeeper) {
statuses = {
"All WO": 0,
// context.translation.open: 1,
context.translation.inProgress: 2,
// context.translation.completed: 4,
context.translation.closed: 3,
// "Canceled": 5,
};
}
return Scaffold(
appBar: DefaultAppBar(
title: context.translation.filter,

@ -147,6 +147,13 @@ class _MyRequestsPageState extends State<MyRequestsPage> with SingleTickerProvid
if (context.settingProvider.isUserFlowMedical && !context.userProvider.isNurse) {
requestsList.add(Request(16, "Incidents"));
}
if (context.userProvider.isHouseKeeper) {
requestsList = [
Request(null, context.translation.allWorkOrder),
Request(5, context.translation.recurrentWo),
// Request(15, 'Demo'), moved to permission api
];
}
_provider = Provider.of<AllRequestsProvider>(context, listen: false);
_provider!.reset();

@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import '../new_views/app_style/app_color.dart';
class FullScreenImageViewer extends StatelessWidget {
final Widget child;
const FullScreenImageViewer(this.child, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
InteractiveViewer(clipBehavior: Clip.none, panEnabled: true, minScale: 0.5, maxScale: 4.0, child: child).center,
SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: FloatingActionButton(
heroTag: "full-screen-image-viewer",
backgroundColor: AppColor.background(context),
onPressed: () => Navigator.maybePop(context),
hoverColor: Colors.transparent,
splashColor: Colors.transparent,
shape: RoundedRectangleBorder(
side: BorderSide(color: AppColor.borderColor(context)),
borderRadius: BorderRadius.circular(14.0),
),
child: Icon(Icons.clear, size: 24, color: AppColor.icon2Color(context)),
),
),
),
],
),
);
}
}
Loading…
Cancel
Save