Merge branch 'refs/heads/main_production_ai_enhancement_rollout_changes' into ui_ux_rollout_merge
# Conflicts: # lib/controllers/api_routes/urls.dart # lib/dashboard_latest/widgets/progress_fragment.dart # lib/dashboard_latest/widgets/request_category_fragment.dart # lib/models/new_models/dashboard_count.dart # lib/modules/cleaner_module/pages/cleaner_request_item_view.dart # lib/modules/cleaner_module/pages/cleaner_search_form_view.dart # lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart # lib/modules/pm_module/recurrent_wo/components/task_info_widget.dart # lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart # lib/modules/tm_module/device_transfer/device_filter_screen.dart # lib/modules/traf_module/traf_request_detail_page.dart # lib/modules/traf_module/traf_request_item_view.dart # lib/new_views/pages/land_page/create_request_type_bottom_sheet.dart # lib/new_views/pages/land_page/requests/recurrent_wo_item_view.dart # lib/new_views/pages/land_page/widgets/request_item_view_list.dartui_ux_rollout_merge
commit
d136fd990e
@ -0,0 +1,62 @@
|
|||||||
|
import '../base.dart';
|
||||||
|
|
||||||
|
class TrafCustomer extends Base {
|
||||||
|
int? customerId;
|
||||||
|
int? langId;
|
||||||
|
String? nameValue;
|
||||||
|
String? customerName;
|
||||||
|
String? languageName;
|
||||||
|
int? id;
|
||||||
|
String? createdBy;
|
||||||
|
String? createdDate;
|
||||||
|
String? modifiedBy;
|
||||||
|
String? modifiedDate;
|
||||||
|
|
||||||
|
TrafCustomer({
|
||||||
|
this.customerId,
|
||||||
|
this.langId,
|
||||||
|
this.nameValue,
|
||||||
|
this.customerName,
|
||||||
|
this.languageName,
|
||||||
|
this.id,
|
||||||
|
this.createdBy,
|
||||||
|
this.createdDate,
|
||||||
|
this.modifiedBy,
|
||||||
|
this.modifiedDate,
|
||||||
|
}) : super(identifier: id?.toString() ?? '', name: nameValue ?? customerName);
|
||||||
|
|
||||||
|
factory TrafCustomer.fromJson(Map<String, dynamic> json) {
|
||||||
|
return TrafCustomer(
|
||||||
|
customerId: json['customerId'],
|
||||||
|
langId: json['langId'],
|
||||||
|
nameValue: json['nameValue'],
|
||||||
|
customerName: json['customerName'],
|
||||||
|
languageName: json['languageName'],
|
||||||
|
id: json['id'],
|
||||||
|
createdBy: json['createdBy'],
|
||||||
|
createdDate: json['createdDate'],
|
||||||
|
modifiedBy: json['modifiedBy'],
|
||||||
|
modifiedDate: json['modifiedDate'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'customerId': customerId,
|
||||||
|
'langId': langId,
|
||||||
|
'nameValue': nameValue,
|
||||||
|
'customerName': customerName,
|
||||||
|
'languageName': languageName,
|
||||||
|
'id': id,
|
||||||
|
'createdBy': createdBy,
|
||||||
|
'createdDate': createdDate,
|
||||||
|
'modifiedBy': modifiedBy,
|
||||||
|
'modifiedDate': modifiedDate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return nameValue ?? customerName ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,24 +0,0 @@
|
|||||||
// Cleaner request search model for mock/static data
|
|
||||||
class CleanerRequest {
|
|
||||||
final String priority;
|
|
||||||
final String date;
|
|
||||||
final String time;
|
|
||||||
final String roomNo;
|
|
||||||
final String site;
|
|
||||||
final String building;
|
|
||||||
final String floor;
|
|
||||||
final String clinic;
|
|
||||||
final String? taskNo;
|
|
||||||
|
|
||||||
CleanerRequest({
|
|
||||||
required this.priority,
|
|
||||||
required this.date,
|
|
||||||
required this.time,
|
|
||||||
required this.roomNo,
|
|
||||||
required this.site,
|
|
||||||
required this.building,
|
|
||||||
required this.floor,
|
|
||||||
required this.clinic,
|
|
||||||
this.taskNo,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -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,68 +0,0 @@
|
|||||||
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/new_views/app_style/app_color.dart';
|
|
||||||
import '../models/cleaner_task_search_result.dart';
|
|
||||||
|
|
||||||
class CleanerRequestItemView extends StatelessWidget {
|
|
||||||
final CleanerTaskSearchResult result;
|
|
||||||
final VoidCallback? onTap;
|
|
||||||
final bool showShadow;
|
|
||||||
|
|
||||||
const CleanerRequestItemView({Key? key, required this.result, this.onTap, this.showShadow = true}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// Task Number and Date Row
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
if (result.taskNo != null && result.taskNo!.isNotEmpty) result.taskNo!.heading5(context).custom(color: AppColor.textStyleColor(context)),
|
|
||||||
if (result.createdDate != null)
|
|
||||||
Text(
|
|
||||||
result.createdDate!.split('T').first,
|
|
||||||
textAlign: TextAlign.end,
|
|
||||||
style: AppTextStyles.tinyFont.copyWith(
|
|
||||||
color: context.isDark ? AppColor.neutral10 : AppColor.neutral50,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
8.height,
|
|
||||||
|
|
||||||
// Location Information
|
|
||||||
if (result.siteName != null && result.siteName!.isNotEmpty) 'Site: ${result.siteName}'.bodyText(context),
|
|
||||||
if (result.buildingName != null && result.buildingName!.isNotEmpty) 'Building: ${result.buildingName}'.bodyText(context),
|
|
||||||
if (result.floorName != null && result.floorName!.isNotEmpty) 'Floor: ${result.floorName}'.bodyText(context),
|
|
||||||
if (result.departmentName != null && result.departmentName!.isNotEmpty) 'Department: ${result.departmentName}'.bodyText(context),
|
|
||||||
if (result.roomName != null && result.roomName!.isNotEmpty) 'Room: ${result.roomName}'.bodyText(context),
|
|
||||||
if (result.locationDescription != null && result.locationDescription!.isNotEmpty) 'Location: ${result.locationDescription}'.bodyText(context),
|
|
||||||
|
|
||||||
8.height,
|
|
||||||
|
|
||||||
// View Details Link
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
context.translation.viewDetails,
|
|
||||||
style: AppTextStyles.bodyText.copyWith(color: AppColor.blueStatus(context)),
|
|
||||||
),
|
|
||||||
4.width,
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_forward,
|
|
||||||
color: AppColor.blueStatus(context),
|
|
||||||
size: 14,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).toShadowContainer(context, withShadow: showShadow).onPress(onTap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,186 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:test_sa/extensions/context_extension.dart';
|
|
||||||
import 'package:test_sa/extensions/string_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/building.dart';
|
|
||||||
import 'package:test_sa/models/new_models/department_room_combined.dart';
|
|
||||||
import 'package:test_sa/models/new_models/floor.dart';
|
|
||||||
import 'package:test_sa/modules/cleaner_module/models/cleaner_task_search_result.dart';
|
|
||||||
import 'package:test_sa/modules/cleaner_module/pages/cleaner_search_details_view.dart';
|
|
||||||
import 'package:test_sa/modules/cleaner_module/provider/cleaner_provider.dart';
|
|
||||||
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
|
|
||||||
import 'package:test_sa/views/widgets/loaders/app_loading.dart';
|
|
||||||
import 'package:test_sa/views/widgets/loaders/no_data_found.dart';
|
|
||||||
|
|
||||||
import 'cleaner_request_item_view.dart';
|
|
||||||
|
|
||||||
class CleanerSearchListView extends StatefulWidget {
|
|
||||||
static const String routeName = '/cleaner_search_list';
|
|
||||||
|
|
||||||
const CleanerSearchListView({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<CleanerSearchListView> createState() => _CleanerSearchListViewState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CleanerSearchListViewState extends State<CleanerSearchListView> {
|
|
||||||
List<CleanerTaskSearchResult> _searchResults = [];
|
|
||||||
bool _isLoading = false;
|
|
||||||
bool _isLoadingMore = false;
|
|
||||||
bool _hasMoreData = true;
|
|
||||||
int _totalRows = 0; // Add this to track total rows from API
|
|
||||||
|
|
||||||
// Pagination
|
|
||||||
int _currentPage = 1;
|
|
||||||
final int _pageSize = 10;
|
|
||||||
|
|
||||||
// Search filters (stored for pagination)
|
|
||||||
Hospital? _selectedSite;
|
|
||||||
Building? _selectedBuilding;
|
|
||||||
Floor? _selectedFloor;
|
|
||||||
Lookup? _selectedLocationDescription;
|
|
||||||
DepartmentRoomCombined? _selectedDepartmentRoom;
|
|
||||||
|
|
||||||
final ScrollController _scrollController = ScrollController();
|
|
||||||
final CleanerProvider _cleanerProvider = CleanerProvider();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_scrollController.addListener(_onScroll);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_scrollController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeDependencies() {
|
|
||||||
super.didChangeDependencies();
|
|
||||||
// Get the search results and filters from navigation arguments
|
|
||||||
final args = ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?;
|
|
||||||
if (args != null && args['searchResults'] != null) {
|
|
||||||
final results = args['searchResults'] as List<dynamic>;
|
|
||||||
_searchResults = results.map((json) => CleanerTaskSearchResult.fromJson(json)).toList();
|
|
||||||
|
|
||||||
// Get totalRows from API response
|
|
||||||
_totalRows = args['totalRows'] as int? ?? 0;
|
|
||||||
|
|
||||||
// Store search filters for pagination
|
|
||||||
_selectedSite = args['selectedSite'] as Hospital?;
|
|
||||||
_selectedBuilding = args['selectedBuilding'] as Building?;
|
|
||||||
_selectedFloor = args['selectedFloor'] as Floor?;
|
|
||||||
_selectedLocationDescription = args['selectedLocationDescription'] as Lookup?;
|
|
||||||
_selectedDepartmentRoom = args['selectedDepartmentRoom'] as DepartmentRoomCombined?;
|
|
||||||
|
|
||||||
// Check if there's more data based on totalRows
|
|
||||||
_hasMoreData = _searchResults.length < _totalRows;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onScroll() {
|
|
||||||
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) {
|
|
||||||
// User is near the bottom, load more data
|
|
||||||
if (!_isLoadingMore && _hasMoreData && !_isLoading) {
|
|
||||||
_loadMoreData();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadMoreData() async {
|
|
||||||
if (_isLoadingMore) return;
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_isLoadingMore = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
_currentPage++;
|
|
||||||
|
|
||||||
final response = await _cleanerProvider.searchPlanRecurrentTasksList(
|
|
||||||
pageSize: _pageSize,
|
|
||||||
pageNumber: _currentPage,
|
|
||||||
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? ?? 0;
|
|
||||||
|
|
||||||
final newResults = results.map((json) => CleanerTaskSearchResult.fromJson(json)).toList();
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_searchResults.addAll(newResults);
|
|
||||||
_totalRows = totalRows;
|
|
||||||
// Check if we have loaded all data based on totalRows
|
|
||||||
_hasMoreData = _searchResults.length < _totalRows;
|
|
||||||
_isLoadingMore = false;
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
setState(() {
|
|
||||||
_isLoadingMore = false;
|
|
||||||
});
|
|
||||||
debugPrint('Error loading more data: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: DefaultAppBar(
|
|
||||||
title: 'Search Results'.addTranslation,
|
|
||||||
onBackPress: () => Navigator.pop(context),
|
|
||||||
),
|
|
||||||
body: SafeArea(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: (_searchResults.isEmpty && !_isLoading)
|
|
||||||
? NoDataFound(message: context.translation.noDataFound).center
|
|
||||||
: ListView.separated(
|
|
||||||
controller: _scrollController,
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
itemCount: _searchResults.length + (_isLoadingMore ? 1 : 0),
|
|
||||||
separatorBuilder: (_, __) => const SizedBox(height: 16),
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
if (_isLoading && _searchResults.isEmpty) {
|
|
||||||
return const SizedBox().toRequestShimmer(context, _isLoading);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show loading indicator at the bottom
|
|
||||||
if (index == _searchResults.length) {
|
|
||||||
return const ALoading().center;
|
|
||||||
}
|
|
||||||
|
|
||||||
final result = _searchResults[index];
|
|
||||||
|
|
||||||
return CleanerRequestItemView(
|
|
||||||
result: result,
|
|
||||||
onTap: () {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => CleanerSearchDetailsView(
|
|
||||||
requestId: result.id ?? 0,
|
|
||||||
taskResult: result,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,410 @@
|
|||||||
|
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/extensions/text_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/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: [
|
||||||
|
Text(
|
||||||
|
'Task Information',
|
||||||
|
style: AppTextStyles.heading5.copyWith(
|
||||||
|
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
12.height,
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (taskData.oneTimeSetting?.taskTypeName != null) _buildInfoRow(context, 'Task Type', taskData.oneTimeSetting!.taskTypeName!),
|
||||||
|
if (taskData.siteName != null) _buildInfoRow(context, 'Site', taskData.siteName!),
|
||||||
|
if (taskData.buildingName != null) _buildInfoRow(context, 'Building', taskData.buildingName!),
|
||||||
|
if (taskData.floorName != null) _buildInfoRow(context, 'Floor', taskData.floorName!),
|
||||||
|
if (taskData.departmentName != null) _buildInfoRow(context, 'Department', taskData.departmentName!),
|
||||||
|
if (taskData.roomName != null) _buildInfoRow(context, 'Room', taskData.roomName!),
|
||||||
|
if (taskData.locationDescription != null) _buildInfoRow(context, 'Location', 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: [
|
||||||
|
Text(
|
||||||
|
'Working Hours',
|
||||||
|
style: AppTextStyles.heading5.copyWith(
|
||||||
|
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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: [
|
||||||
|
Text(
|
||||||
|
'Total Working Hours',
|
||||||
|
style: AppTextStyles.bodyText.copyWith(
|
||||||
|
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
CMRequestUtils.formatTotalWorkingHours(totalWorkingHours),
|
||||||
|
style: AppTextStyles.heading6.copyWith(
|
||||||
|
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).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: [
|
||||||
|
Text(
|
||||||
|
'Checklist',
|
||||||
|
style: AppTextStyles.heading5.copyWith(
|
||||||
|
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${completedInstructions.length}/$totalInstructions',
|
||||||
|
style: AppTextStyles.bodyText.copyWith(
|
||||||
|
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
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: Text(
|
||||||
|
instruction.instructionText ?? '',
|
||||||
|
style: AppTextStyles.bodyText.copyWith(
|
||||||
|
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
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: [
|
||||||
|
Text(
|
||||||
|
'Comment:',
|
||||||
|
style: AppTextStyles.tinyFont.copyWith(
|
||||||
|
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
4.height,
|
||||||
|
Text(
|
||||||
|
instruction.comment!,
|
||||||
|
style: AppTextStyles.bodyText.copyWith(
|
||||||
|
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).paddingAll(16),
|
||||||
|
],
|
||||||
|
).toShadowContainer(context, padding: 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildImagesCard(BuildContext context) {
|
||||||
|
// Show captured images (base64 from camera)
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Captured Images',
|
||||||
|
style: AppTextStyles.heading5.copyWith(
|
||||||
|
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
16.height,
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: taskData.imageRooms!.map((imageString) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
// Show full screen image
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => Scaffold(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
iconTheme: const IconThemeData(color: Colors.white),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: InteractiveViewer(
|
||||||
|
child: _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: [
|
||||||
|
Text(
|
||||||
|
'Attachments',
|
||||||
|
style: AppTextStyles.heading5.copyWith(
|
||||||
|
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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: Text(
|
||||||
|
'$label:',
|
||||||
|
style: AppTextStyles.bodyText.copyWith(
|
||||||
|
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
value,
|
||||||
|
style: AppTextStyles.bodyText.copyWith(
|
||||||
|
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,472 @@
|
|||||||
|
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/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),
|
||||||
|
16.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(_taskData!.statusName!),
|
||||||
|
backgroundColor: AppColor.getHistoryLogStatusColorByName(_taskData!.statusName!),
|
||||||
|
),
|
||||||
|
if (_taskData?.createdDate != null)
|
||||||
|
Text(
|
||||||
|
_taskData!.createdDate!.toString().toServiceRequestCardFormat,
|
||||||
|
textAlign: TextAlign.end,
|
||||||
|
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 using AppTimer widget
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
).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() {
|
||||||
|
// 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 ?? [];
|
||||||
|
print('DEBUG: Captured images count: ${capturedImages.length}');
|
||||||
|
print('DEBUG: Images: $capturedImages');
|
||||||
|
|
||||||
|
if (capturedImages.isEmpty) {
|
||||||
|
_showErrorDialog(context, "Please capture at least one image before completing");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate working hours
|
||||||
|
double totalWorkingHours = 0;
|
||||||
|
if (_timerStart != null) {
|
||||||
|
final duration = DateTime.now().difference(_timerStart!);
|
||||||
|
totalWorkingHours = duration.inSeconds / 3600.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to preview screen
|
||||||
|
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() ??
|
||||||
|
[];
|
||||||
|
|
||||||
|
// Convert images to attachments format
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,85 +0,0 @@
|
|||||||
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/models/new_models/floor.dart';
|
|
||||||
|
|
||||||
class FmsFloorProvider extends ChangeNotifier {
|
|
||||||
//reset provider data
|
|
||||||
void reset() {
|
|
||||||
_floors = null;
|
|
||||||
_stateCode = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// state code of current request to defied error message
|
|
||||||
int? _stateCode;
|
|
||||||
int? get stateCode => _stateCode;
|
|
||||||
|
|
||||||
// contain floors data
|
|
||||||
List<Floor>? _floors;
|
|
||||||
List<Floor>? get floors => _floors;
|
|
||||||
|
|
||||||
// loading state
|
|
||||||
bool _loading = false;
|
|
||||||
bool get isLoading => _loading;
|
|
||||||
|
|
||||||
set isLoading(bool isLoading) {
|
|
||||||
_loading = isLoading;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get floors by site and building ID
|
|
||||||
Future<int> getFloorsBySiteAndBuilding(int siteId, int buildingId) async {
|
|
||||||
if (_loading == true) return -2;
|
|
||||||
_loading = true;
|
|
||||||
notifyListeners();
|
|
||||||
Response response;
|
|
||||||
try {
|
|
||||||
response = await ApiManager.instance.post(
|
|
||||||
URLs.getCustomerFloors,
|
|
||||||
body: {
|
|
||||||
"siteId": siteId,
|
|
||||||
"buildingId": buildingId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
_stateCode = response.statusCode;
|
|
||||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
||||||
// client's request was successfully received
|
|
||||||
List floorsListJson = json.decode(response.body)["data"];
|
|
||||||
_floors = floorsListJson.map((floor) => Floor.fromJson(floor)).toList();
|
|
||||||
}
|
|
||||||
_loading = false;
|
|
||||||
notifyListeners();
|
|
||||||
return response.statusCode;
|
|
||||||
} catch (error) {
|
|
||||||
_loading = false;
|
|
||||||
_stateCode = -1;
|
|
||||||
notifyListeners();
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get floors list directly without provider state
|
|
||||||
Future<List<Floor>> getFloorsListBySiteAndBuilding(int siteId, int buildingId) async {
|
|
||||||
Response response;
|
|
||||||
try {
|
|
||||||
response = await ApiManager.instance.post(
|
|
||||||
URLs.getCustomerFloors,
|
|
||||||
body: {
|
|
||||||
"siteId": siteId,
|
|
||||||
"buildingId": buildingId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
||||||
List floorsListJson = json.decode(response.body)["data"];
|
|
||||||
return floorsListJson.map((floor) => Floor.fromJson(floor)).toList();
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
} catch (error) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,140 +0,0 @@
|
|||||||
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/models/lookup.dart';
|
|
||||||
|
|
||||||
class LocationDescriptionResponse {
|
|
||||||
List<Lookup>? locationDescriptions;
|
|
||||||
List<DepartmentRoomItem>? deptRoom;
|
|
||||||
|
|
||||||
LocationDescriptionResponse({
|
|
||||||
this.locationDescriptions,
|
|
||||||
this.deptRoom,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory LocationDescriptionResponse.fromJson(Map<String, dynamic> json) {
|
|
||||||
return LocationDescriptionResponse(
|
|
||||||
locationDescriptions: json['locationDescriptions'] != null
|
|
||||||
? (json['locationDescriptions'] as List)
|
|
||||||
.map((e) => Lookup(name: e['locationDescriptionName']))
|
|
||||||
.toList()
|
|
||||||
: null,
|
|
||||||
deptRoom: json['deptRoom'] != null
|
|
||||||
? (json['deptRoom'] as List)
|
|
||||||
.map((e) => DepartmentRoomItem.fromJson(e))
|
|
||||||
.toList()
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DepartmentRoomItem {
|
|
||||||
String? locationDescriptionName;
|
|
||||||
String? deptAndRoomName;
|
|
||||||
String? deptName;
|
|
||||||
String? roomName;
|
|
||||||
int? deptId;
|
|
||||||
int? roomId;
|
|
||||||
|
|
||||||
DepartmentRoomItem({
|
|
||||||
this.locationDescriptionName,
|
|
||||||
this.deptAndRoomName,
|
|
||||||
this.deptName,
|
|
||||||
this.roomName,
|
|
||||||
this.deptId,
|
|
||||||
this.roomId,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory DepartmentRoomItem.fromJson(Map<String, dynamic> json) {
|
|
||||||
return DepartmentRoomItem(
|
|
||||||
locationDescriptionName: json['locationDescriptionName'],
|
|
||||||
deptAndRoomName: json['deptAndRoomName'],
|
|
||||||
deptName: json['deptName'],
|
|
||||||
roomName: json['roomName'],
|
|
||||||
deptId: json['deptId'],
|
|
||||||
roomId: json['roomId'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FmsLocationDescriptionProvider extends ChangeNotifier {
|
|
||||||
//reset provider data
|
|
||||||
void reset() {
|
|
||||||
_locationDescriptions = null;
|
|
||||||
_deptRoomList = null;
|
|
||||||
_stateCode = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// state code of current request to defied error message
|
|
||||||
int? _stateCode;
|
|
||||||
int? get stateCode => _stateCode;
|
|
||||||
|
|
||||||
// contain location descriptions data
|
|
||||||
List<Lookup>? _locationDescriptions;
|
|
||||||
List<Lookup>? get locationDescriptions => _locationDescriptions;
|
|
||||||
|
|
||||||
// contain dept room list
|
|
||||||
List<DepartmentRoomItem>? _deptRoomList;
|
|
||||||
List<DepartmentRoomItem>? get deptRoomList => _deptRoomList;
|
|
||||||
|
|
||||||
// loading state
|
|
||||||
bool _loading = false;
|
|
||||||
bool get isLoading => _loading;
|
|
||||||
|
|
||||||
set isLoading(bool isLoading) {
|
|
||||||
_loading = isLoading;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get location descriptions by site, building, and floor ID
|
|
||||||
Future<int> getLocationDescriptions(int siteId, int buildingId, int floorId) async {
|
|
||||||
if (_loading == true) return -2;
|
|
||||||
_loading = true;
|
|
||||||
notifyListeners();
|
|
||||||
Response response;
|
|
||||||
try {
|
|
||||||
response = await ApiManager.instance.get(
|
|
||||||
"${URLs.getCustomerLocationDescription}?siteId=$siteId&buildingId=$buildingId&floorId=$floorId",
|
|
||||||
enableToastMessage: false,
|
|
||||||
);
|
|
||||||
_stateCode = response.statusCode;
|
|
||||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
||||||
// client's request was successfully received
|
|
||||||
final jsonData = json.decode(response.body)["data"];
|
|
||||||
LocationDescriptionResponse locationResponse = LocationDescriptionResponse.fromJson(jsonData);
|
|
||||||
_locationDescriptions = locationResponse.locationDescriptions;
|
|
||||||
_deptRoomList = locationResponse.deptRoom;
|
|
||||||
}
|
|
||||||
_loading = false;
|
|
||||||
notifyListeners();
|
|
||||||
return response.statusCode;
|
|
||||||
} catch (error) {
|
|
||||||
_loading = false;
|
|
||||||
_stateCode = -1;
|
|
||||||
notifyListeners();
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get location descriptions and dept room list directly without provider state
|
|
||||||
Future<LocationDescriptionResponse?> getLocationDescriptionsList(int siteId, int buildingId, int floorId) async {
|
|
||||||
Response response;
|
|
||||||
try {
|
|
||||||
response = await ApiManager.instance.get(
|
|
||||||
"${URLs.getCustomerLocationDescription}?siteId=$siteId&buildingId=$buildingId&floorId=$floorId",
|
|
||||||
enableToastMessage: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
||||||
final jsonData = json.decode(response.body)["data"];
|
|
||||||
return LocationDescriptionResponse.fromJson(jsonData);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,218 @@
|
|||||||
|
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 {
|
||||||
|
_showFullImage(context, 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showFullImage(BuildContext context, String imageUrl) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => Scaffold(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
body: SafeArea(
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: InteractiveViewer(
|
||||||
|
child: _getImageWidget(imageUrl),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const ABackButton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,137 @@
|
|||||||
|
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/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,
|
||||||
|
Text(
|
||||||
|
requestData!.transactionDate?.toServiceRequestCardFormat ?? "",
|
||||||
|
textAlign: TextAlign.end,
|
||||||
|
style: AppTextStyles.tinyFont.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral50),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
8.height,
|
||||||
|
(requestData?.typeTransaction ?? context.translation.recurrentWo).heading5(context),
|
||||||
|
8.height,
|
||||||
|
'${context.translation.taskNo}: ${requestData!.requestNo}'.bodyText(context),
|
||||||
|
if (requestData?.siteName != null) '${context.translation.site}: ${requestData!.siteName}'.bodyText(context),
|
||||||
|
if (requestData!.buildingName != null) '${context.translation.building}: ${requestData!.buildingName}'.bodyText(context),
|
||||||
|
if (requestData!.departmentName != null) '${context.translation.department}: ${requestData!.departmentName}'.bodyText(context),
|
||||||
|
if (requestData!.floorName != null) '${context.translation.floor}: ${requestData!.floorName}'.bodyText(context),
|
||||||
|
if (requestData!.roomName != null) '${context.translation.room}: ${requestData!.roomName}'.bodyText(context),
|
||||||
|
8.height,
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
context.translation.viewDetails,
|
||||||
|
style: AppTextStyles.bodyText.copyWith(color: AppColor.blueStatus(context)),
|
||||||
|
),
|
||||||
|
4.width,
|
||||||
|
Icon(Icons.arrow_forward, color: AppColor.blueStatus(context), size: 14)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).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,
|
||||||
|
Text(
|
||||||
|
requestDetails!.date?.toServiceRequestCardFormat ?? "",
|
||||||
|
textAlign: TextAlign.end,
|
||||||
|
style: AppTextStyles.tinyFont.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral50),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
8.height,
|
||||||
|
(requestDetails?.nameOfType ?? context.translation.recurrentWo).heading5(context),
|
||||||
|
8.height,
|
||||||
|
'${context.translation.site}: ${requestDetails!.site}'.bodyText(context),
|
||||||
|
'${context.translation.taskNo}: ${requestDetails!.requestNo}'.bodyText(context),
|
||||||
|
if (requestDetails!.siteName != null) '${context.translation.site}: ${requestDetails!.siteName}'.bodyText(context),
|
||||||
|
if (requestDetails!.buildingName != null) '${context.translation.building}: ${requestDetails!.buildingName}'.bodyText(context),
|
||||||
|
if (requestDetails!.departmentName != null) '${context.translation.department}: ${requestDetails!.departmentName}'.bodyText(context),
|
||||||
|
if (requestDetails!.floorName != null) '${context.translation.floor}: ${requestDetails!.floorName}'.bodyText(context),
|
||||||
|
if (requestDetails!.roomName != null) '${context.translation.room}: ${requestDetails!.roomName}'.bodyText(context),
|
||||||
|
8.height,
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
context.translation.viewDetails,
|
||||||
|
style: AppTextStyles.bodyText.copyWith(color: AppColor.blueStatus(context)),
|
||||||
|
),
|
||||||
|
4.width,
|
||||||
|
Icon(Icons.arrow_forward, color: AppColor.blueStatus(context), size: 14)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).toShadowContainer(context, withShadow: showShadow).onPress(() async {
|
||||||
|
// Navigate to editable view for in-progress tasks
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => OneTimeTaskView(
|
||||||
|
taskId: requestDetails!.id ?? 0,
|
||||||
|
initialData: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
import 'package:test_sa/models/new_models/traf_customer.dart';
|
||||||
|
import 'package:test_sa/providers/loading_list_notifier.dart';
|
||||||
|
|
||||||
|
import '../../controllers/api_routes/api_manager.dart';
|
||||||
|
import '../../controllers/api_routes/urls.dart';
|
||||||
|
|
||||||
|
class CustomerLookupProvider extends LoadingListNotifier<TrafCustomer> {
|
||||||
|
@override
|
||||||
|
Future getData({int? id}) async {}
|
||||||
|
|
||||||
|
Future getDataByUserId(String? userId) async {
|
||||||
|
if (loading == true) return -2;
|
||||||
|
if (userId == null || userId.isEmpty) return -1;
|
||||||
|
|
||||||
|
loading = true;
|
||||||
|
notifyListeners();
|
||||||
|
try {
|
||||||
|
Response response = await ApiManager.instance.get(URLs.getCustomersByUserId + "?userId=$userId", useCache: true, enableToastMessage: false);
|
||||||
|
stateCode = response.statusCode;
|
||||||
|
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||||
|
List categoriesListJson = json.decode(response.body)["data"];
|
||||||
|
items = categoriesListJson.map((item) => TrafCustomer.fromJson(item)).toList();
|
||||||
|
}
|
||||||
|
loading = false;
|
||||||
|
notifyListeners();
|
||||||
|
return response.statusCode;
|
||||||
|
} catch (error) {
|
||||||
|
loading = false;
|
||||||
|
stateCode = -1;
|
||||||
|
notifyListeners();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue