new flow api implemented

cleaner_module_new_flow
WaseemAbbasi22 1 month ago
parent 05960f28e0
commit 5c46fdd8e5

@ -71,8 +71,12 @@ class URLs {
static get searchPlanRecurrentTaskListSearch => "$_baseUrl/PlanRecurrentTasks/PlanRecurrentTaskListSearchDto"; // get
static get checkQRSameRoom => "$_baseUrl/PlanRecurrentTasks/CheckQRSameRoom"; // get
//
//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";
//service request new flow urls.
static get nurseDashboardCountUrl => '$_baseUrl/ServiceRequest/GetDashboardNurseCount';

@ -0,0 +1,236 @@
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 captured image
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,
});
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'],
);
}
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,
};
}
}
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? fileName;
String? fileUrl;
String? fileType;
Attachment({
this.id,
this.fileName,
this.fileUrl,
this.fileType,
});
factory Attachment.fromJson(Map<String, dynamic> json) {
return Attachment(
id: json['id'],
fileName: json['fileName'],
fileUrl: json['fileUrl'],
fileType: json['fileType'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'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,27 +1,21 @@
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/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/qr/scan_qr.dart';
class CleanerSearchFormView extends StatefulWidget {
static const String routeName = '/cleaner_search';
@ -33,165 +27,140 @@ 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 sites. Please try again.');
_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);
setState(() {
_buildingOptions = buildings;
_isLoadingBuildings = false;
});
} catch (e) {
if (result != null && result is String && result.isNotEmpty) {
setState(() {
_isLoadingBuildings = false;
_buildingOptions = [];
_isScanning = true;
});
_showErrorDialog(context, 'Failed to load buildings. Please try again.');
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 = [];
});
_showErrorDialog(context, 'Failed to load floors. Please try again.');
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 = [];
});
_showErrorDialog(context, 'Failed to load location descriptions. Please try again.');
}
}
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();
}
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,
);
void _autoSelectDepartmentRoom() {
final combinations = _getDepartmentRoomCombinations();
final isSuccess = result["success"] ?? false;
final data = result["data"];
final message = result["message"] ?? "Unknown error occurred";
if (combinations.isEmpty) {
setState(() {
selectedDepartmentRoom = null;
});
return;
}
if (isSuccess && data != null) {
// Get the task ID from the response
final taskId = data['id'];
// If only one item, auto-fill
if (combinations.length == 1) {
setState(() {
selectedDepartmentRoom = combinations.first;
});
} else {
// If more than one, auto-select first item
if (mounted && taskId != null) {
// Navigate to OneTimeTaskView
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => OneTimeTaskView(taskId: taskId),
),
);
}
} 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;
});
}
}
@ -213,253 +182,92 @@ class _CleanerSearchFormViewState extends State<CleanerSearchFormView> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Site Autocomplete Field
Container(
padding: EdgeInsets.symmetric(
horizontal: 16.toScreenWidth,
vertical: 8.toScreenHeight,
),
decoration: BoxDecoration(
color: AppColor.fieldBgColor(context),
borderRadius: BorderRadius.circular(10),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Site',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral20,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 4.toScreenHeight),
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,
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.black10,
),
decoration: InputDecoration(
hintText: 'Search site...',
hintStyle: AppTextStyles.bodyText.copyWith(
color: AppColor.neutral20,
),
filled: false,
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding: EdgeInsets.zero,
isDense: true,
),
onChanged: (value) {
_siteController.text = value;
},
),
),
if (_isLoadingSites)
const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: AppColor.primary10,
),
)
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: const Icon(
Icons.clear,
color: AppColor.neutral20,
size: 20,
),
),
],
);
},
),
),
],
),
),
const SizedBox(height: 18),
// 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();
enabled: _taskTypeOptions.isNotEmpty,
loading: _isLoadingTaskTypes,
staticData: _taskTypeOptions,
onSelect: (value) {
setState(() {
_siteOptions = [];
print('site options length ${_siteOptions.length}');
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());
}
},
),
const SizedBox(height: 18),
// 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 ? Colors.white : Colors.grey[300],
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: selectedTaskType != null ? AppColor.blueStatus(context) : Colors.grey,
width: 2,
),
),
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
? (context.isDark ? AppColor.neutral20 : AppColor.neutral120)
: 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
? (context.isDark ? AppColor.neutral20 : AppColor.black20)
: Colors.grey,
):'Re Scan QR Code'.bodyText(context).custom(
color: selectedTaskType != null
? (context.isDark ? AppColor.neutral20 : AppColor.black20)
: Colors.grey,
),
],
),
),
),
const SizedBox(height: 18),
// 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();
},
),
const SizedBox(height: 18),
// 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,
),
),
],
@ -467,52 +275,69 @@ class _CleanerSearchFormViewState extends State<CleanerSearchFormView> {
);
}
Future<void> _onSubmit() async {
// Validate that at least site is selected
if (selectedSite == null) {
_showErrorDialog(context, 'Please select a site');
return;
}
Widget _buildLocationDataCard(BuildContext context) {
final locationData = _scannedLocationData;
try {
setState(() {
_isSearching = true;
});
// 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,
);
final results = response['data'] as List<dynamic>;
final totalRows = response['totalRows'] as int;
// 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) {
_showErrorDialog(context, 'Error searching. Please try again.');
} finally {
setState(() {
_isSearching = false;
});
if (locationData == null) {
return const SizedBox();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (locationData.siteName != null && locationData.siteName!.isNotEmpty)
Text(
'Site: ${locationData.siteName}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
if (locationData.buildingName != null && locationData.buildingName!.isNotEmpty)
Text(
'Building: ${locationData.buildingName}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
if (locationData.floorName != null && locationData.floorName!.isNotEmpty)
Text(
'Floor: ${locationData.floorName}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
if (locationData.departmentName != null && locationData.departmentName!.isNotEmpty)
Text(
'Department: ${locationData.departmentName}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
if (locationData.roomName != null && locationData.roomName!.isNotEmpty)
Text(
'Room: ${locationData.roomName}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
],
),
],
),
],
).paddingOnly(start: 16, end: 16, top: 16, bottom: 16),
],
).toShadowContainer(context, padding: 0);
}
void _showErrorDialog(BuildContext context, String message) {

@ -0,0 +1,437 @@
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/text_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/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/widgets/cleaner_preview_dialog.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/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';
class OneTimeTaskView extends StatefulWidget {
static const String routeName = "/one_time_task";
final int taskId;
const OneTimeTaskView({Key? key, required this.taskId}) : super(key: key);
@override
State<OneTimeTaskView> createState() => _OneTimeTaskViewState();
}
class _OneTimeTaskViewState extends State<OneTimeTaskView> {
OneTimeTaskData? _taskData;
bool _isLoading = false;
DateTime? _timerStart;
@override
void initState() {
super.initState();
_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']);
_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;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: DefaultAppBar(
title: 'One Time Task',
onWillPopScope: isClosed
? null
: () {
_showExitDialog();
},
),
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),
16.height,
CleanerChecklistWidget(
isClosed: isClosed,
checklistItems: _convertInstructionsToChecklist(),
onChecklistChanged: (value) {
setState(() {
_updateInstructions(value);
});
},
capturedImageBase64: _taskData!.imageRoom,
onImageCaptured: (imageBase64) {
setState(() {
_taskData!.imageRoom = imageBase64;
});
},
),
],
),
),
),
if (!isClosed) ...[
FooterActionButton.footerContainer(
context: context,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
AppFilledButton(
label: context.translation.save,
buttonColor: context.isDark ? AppColor.neutral20 : AppColor.white60,
textColor: context.isDark ? Colors.white : AppColor.black10,
onPressed: () => Navigator.pop(context),
).expanded,
12.width,
AppFilledButton(
label: context.translation.complete,
buttonColor: AppColor.primary10,
onPressed: _onComplete,
).expanded,
],
),
),
]
],
),
)
: NoDataFound(message: context.translation.noDataFound).center,
).handlePopScope(
cxt: context,
showPopUp: !isClosed,
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,
children: [
if (_taskData?.statusName != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: _getStatusColor().withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
_taskData!.statusName!,
style: AppTextStyles.bodyText.copyWith(
color: _getStatusColor(),
fontWeight: FontWeight.w600,
),
),
),
if (_taskData?.createdDate != null)
Text(
_formatDate(_taskData!.createdDate!),
style: AppTextStyles.tinyFont.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
),
),
],
),
12.height,
if (_taskData?.oneTimeSetting?.taskTypeName != null)
Text(
_taskData!.oneTimeSetting!.taskTypeName!,
style: AppTextStyles.heading4.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
),
),
8.height,
if (_taskData?.oneTimeSetting?.instructionDescriptionName != null)
Text(
_taskData!.oneTimeSetting!.instructionDescriptionName!,
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
8.height,
if (_taskData?.siteName != null)
_buildInfoRow('Site', _taskData!.siteName!),
if (_taskData?.buildingName != null)
_buildInfoRow('Building', _taskData!.buildingName!),
if (_taskData?.floorName != null)
_buildInfoRow('Floor', _taskData!.floorName!),
if (_taskData?.departmentName != null)
_buildInfoRow('Department', _taskData!.departmentName!),
if (_taskData?.roomName != null)
_buildInfoRow('Room', _taskData!.roomName!),
if (_taskData?.locationDescription != null)
_buildInfoRow('Location', _taskData!.locationDescription!),
// Timer display
if (_timerStart != null && !isClosed) ...[
12.height,
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColor.primary10.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColor.primary10),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Working Time:',
style: AppTextStyles.bodyText.copyWith(
fontWeight: FontWeight.w600,
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
Text(
_formatDuration(DateTime.now().difference(_timerStart!)),
style: AppTextStyles.heading4.copyWith(
color: AppColor.primary10,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
],
).paddingOnly(start: 16, end: 16, top: 16, bottom: 16),
],
).toShadowContainer(context, padding: 0);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(
'$label: $value',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
);
}
List<ChecklistItem> _convertInstructionsToChecklist() {
return _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() ?? [];
}
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 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;
}
// Show preview dialog
_showPreviewDialog();
}
void _showPreviewDialog() {
// Calculate working hours
double totalWorkingHours = 0;
if (_timerStart != null) {
final duration = DateTime.now().difference(_timerStart!);
totalWorkingHours = duration.inSeconds / 3600.0;
}
CleanerPreviewDialog.show(
context,
model: _convertToRecurrentWoData(),
totalWorkingHours: totalWorkingHours,
onConfirm: _performComplete,
);
}
// Convert OneTimeTaskData to RecurrentWoData for preview dialog compatibility
dynamic _convertToRecurrentWoData() {
return {
'siteName': _taskData?.siteName,
'buildingName': _taskData?.buildingName,
'floorName': _taskData?.floorName,
'departmentName': _taskData?.departmentName,
'roomName': _taskData?.roomName,
'locationDescription': _taskData?.locationDescription,
'taskNo': 'OT-${_taskData?.id}',
'title': _taskData?.oneTimeSetting?.taskTypeName,
};
}
Future<void> _performComplete() async {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const AppLazyLoading(),
);
try {
final instructionsPayload = _taskData?.instructions?.map((instruction) {
return {
'id': instruction.id,
'instructionTextId': instruction.instructionTextId,
'instructionText': instruction.instructionText,
'isRequired': instruction.isRequired,
'isDone': instruction.isDone,
'comment': instruction.comment,
};
}).toList() ?? [];
final result = await CleanerProvider().completeOneTimeTask(
id: _taskData!.id!,
instructions: instructionsPayload,
imageRoom: _taskData?.imageRoom,
);
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 _showExitDialog() {
showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: 'Do you want to exit without completing?',
title: 'Confirm',
onTap: () {
Navigator.pop(context); // Close dialog
Navigator.pop(context); // Exit screen
},
),
);
}
void _showErrorDialog(BuildContext context, String message) {
showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: message,
title: 'Error',
onTap: () => Navigator.pop(context),
),
);
}
Color _getStatusColor() {
if (_taskData?.statusValue == 2) {
return Colors.green;
}
return AppColor.primary10;
}
String _formatDate(String dateStr) {
try {
final date = DateTime.parse(dateStr);
return '${date.day}/${date.month}/${date.year}';
} catch (e) {
return dateStr;
}
}
String _formatDuration(Duration duration) {
final hours = duration.inHours;
final minutes = duration.inMinutes.remainder(60);
final seconds = duration.inSeconds.remainder(60);
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
}

@ -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,
},
);
@ -203,6 +205,183 @@ class CleanerProvider extends ChangeNotifier {
}
}
/// 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',
);
// 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,
String? imageRoom,
}) async {
Response response;
try {
response = await ApiManager.instance.post(
URLs.completeRequest,
body: {
"id": id,
"instructions": instructions,
"imageRoom": imageRoom,
},
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 [];
}
}
}
Loading…
Cancel
Save