housekeeping module completed
parent
3352c73e1d
commit
33f1f07e86
@ -0,0 +1,6 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.10515e-06 1C7.10515e-06 0.447715 0.447722 0 1.00001 0H17C17.5523 0 18 0.447715 18 1C18 1.55228 17.5523 2 17 2H1.00001C0.447722 2 7.10515e-06 1.55228 7.10515e-06 1Z" fill="#212936"/>
|
||||
<path d="M1.00001 6.00001C0.447722 6 3.92408e-06 6.44772 0 7C-3.92403e-06 7.55228 0.447708 8 0.999993 8.00001L12 8.00008C12.5523 8.00009 13 7.55238 13 7.00009C13 6.44781 12.5523 6.00009 12 6.00009L1.00001 6.00001Z" fill="#212936"/>
|
||||
<path d="M1.00001 12C0.447722 12 7.10515e-06 12.4477 7.10515e-06 13C7.10515e-06 13.5523 0.447722 14 1.00001 14H8.00001C8.55229 14 9.00001 13.5523 9.00001 13C9.00001 12.4477 8.55229 12 8.00001 12H1.00001Z" fill="#212936"/>
|
||||
<path d="M15.1616 15.5C14.986 15.4999 14.7703 15.4997 14.5939 15.5218L14.5905 15.5222C14.4641 15.538 13.888 15.6098 13.6137 16.1754C13.3387 16.7422 13.6407 17.2424 13.706 17.3506L14.0451 17.819C14.3398 18.1949 14.7455 18.7093 15.1259 19.1003C15.3157 19.2954 15.533 19.4967 15.7639 19.6555C15.9691 19.7967 16.3193 20 16.75 20C17.1807 20 17.5309 19.7967 17.7361 19.6555C17.967 19.4967 18.1843 19.2954 18.3741 19.1003C18.7546 18.7093 19.1603 18.1949 19.4549 17.8191L19.794 17.3506C19.8594 17.2424 20.1613 16.7422 19.8863 16.1754C19.612 15.6098 19.0359 15.538 18.9095 15.5222L18.9061 15.5218C18.7297 15.4997 18.514 15.4999 18.3384 15.5L17.75 15.5V6.99996C17.75 6.44768 17.3023 5.99996 16.75 5.99996C16.1977 5.99996 15.75 6.44768 15.75 6.99996V15.5L15.1616 15.5Z" fill="#212936"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,55 @@
|
||||
import 'package:test_sa/models/base.dart';
|
||||
|
||||
class DepartmentRoomCombined extends Base {
|
||||
final int? deptId;
|
||||
final int? roomId;
|
||||
final String? deptName;
|
||||
final String? roomName;
|
||||
final String? locationDescriptionName;
|
||||
final String? deptAndRoomName;
|
||||
|
||||
DepartmentRoomCombined({
|
||||
this.deptId,
|
||||
this.roomId,
|
||||
this.deptName,
|
||||
this.roomName,
|
||||
this.locationDescriptionName,
|
||||
this.deptAndRoomName,
|
||||
}) : super(
|
||||
identifier: '${deptId}_${roomId}',
|
||||
name: deptAndRoomName ?? '${deptName ?? ''} / ${roomName ?? ''}',
|
||||
);
|
||||
|
||||
factory DepartmentRoomCombined.fromJson(Map<String, dynamic> json) {
|
||||
return DepartmentRoomCombined(
|
||||
deptId: json['deptId'],
|
||||
roomId: json['roomId'],
|
||||
deptName: json['deptName'],
|
||||
roomName: json['roomName'],
|
||||
locationDescriptionName: json['locationDescriptionName'],
|
||||
deptAndRoomName: json['deptAndRoomName'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'deptId': deptId,
|
||||
'roomId': roomId,
|
||||
'deptName': deptName,
|
||||
'roomName': roomName,
|
||||
'locationDescriptionName': locationDescriptionName,
|
||||
'deptAndRoomName': deptAndRoomName,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is DepartmentRoomCombined &&
|
||||
runtimeType == other.runtimeType &&
|
||||
deptId == other.deptId &&
|
||||
roomId == other.roomId;
|
||||
|
||||
@override
|
||||
int get hashCode => deptId.hashCode ^ roomId.hashCode;
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
// 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,73 @@
|
||||
class CleanerTaskSearchResult {
|
||||
int? id;
|
||||
String? taskNo;
|
||||
String? siteName;
|
||||
String? buildingName;
|
||||
String? floorName;
|
||||
String? departmentName;
|
||||
String? roomName;
|
||||
String? locationDescription;
|
||||
String? createdDate;
|
||||
|
||||
CleanerTaskSearchResult({
|
||||
this.id,
|
||||
this.taskNo,
|
||||
this.siteName,
|
||||
this.buildingName,
|
||||
this.floorName,
|
||||
this.departmentName,
|
||||
this.roomName,
|
||||
this.locationDescription,
|
||||
this.createdDate,
|
||||
});
|
||||
|
||||
factory CleanerTaskSearchResult.fromJson(Map<String, dynamic> json) {
|
||||
return CleanerTaskSearchResult(
|
||||
id: json['id'],
|
||||
taskNo: json['taskNo'],
|
||||
siteName: json['siteName'],
|
||||
buildingName: json['buildingName'],
|
||||
floorName: json['floorName'],
|
||||
departmentName: json['departmentName'],
|
||||
roomName: json['roomName'],
|
||||
locationDescription: json['locationDescription'],
|
||||
createdDate: json['createdDate'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'taskNo': taskNo,
|
||||
'siteName': siteName,
|
||||
'buildingName': buildingName,
|
||||
'floorName': floorName,
|
||||
'departmentName': departmentName,
|
||||
'roomName': roomName,
|
||||
'locationDescription': locationDescription,
|
||||
'createdDate': createdDate,
|
||||
};
|
||||
}
|
||||
|
||||
String get fullLocation {
|
||||
List<String> parts = [];
|
||||
if (siteName != null && siteName!.isNotEmpty) parts.add(siteName!);
|
||||
if (buildingName != null && buildingName!.isNotEmpty) parts.add(buildingName!);
|
||||
if (floorName != null && floorName!.isNotEmpty) parts.add(floorName!);
|
||||
if (departmentName != null && departmentName!.isNotEmpty) parts.add(departmentName!);
|
||||
if (roomName != null && roomName!.isNotEmpty) parts.add('Room $roomName');
|
||||
return parts.join(' - ');
|
||||
}
|
||||
|
||||
String get departmentRoom {
|
||||
if (departmentName != null && roomName != null) {
|
||||
return '$departmentName / $roomName';
|
||||
} else if (departmentName != null) {
|
||||
return departmentName!;
|
||||
} else if (roomName != null) {
|
||||
return 'Room $roomName';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,68 @@
|
||||
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),
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,318 @@
|
||||
// UI: Cleaner Search Details View
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.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/new_views/app_style/app_color.dart';
|
||||
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
|
||||
import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart';
|
||||
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
|
||||
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
|
||||
import 'package:test_sa/views/widgets/qr/scan_qr.dart';
|
||||
import '../models/cleaner_task_search_result.dart';
|
||||
import '../provider/cleaner_provider.dart';
|
||||
import '../../pm_module/recurrent_wo/recurrent_work_order_view.dart';
|
||||
import 'package:test_sa/controllers/providers/api/all_requests_provider.dart';
|
||||
|
||||
class CleanerSearchDetailsView extends StatefulWidget {
|
||||
static const String routeName = '/cleaner_search_details';
|
||||
final int requestId;
|
||||
final CleanerTaskSearchResult? taskResult;
|
||||
|
||||
const CleanerSearchDetailsView({
|
||||
Key? key,
|
||||
required this.requestId,
|
||||
this.taskResult,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_CleanerSearchDetailsViewState createState() => _CleanerSearchDetailsViewState();
|
||||
}
|
||||
|
||||
class _CleanerSearchDetailsViewState extends State<CleanerSearchDetailsView> {
|
||||
late CleanerProvider _cleanerProvider;
|
||||
bool _isQRScanned = false;
|
||||
CleanerTaskSearchResult? _taskData;
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_cleanerProvider = Provider.of<CleanerProvider>(context, listen: false);
|
||||
_taskData = widget.taskResult;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
getInitialData();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> getInitialData() async {
|
||||
// If taskResult is null, fetch data from recurrent work order API
|
||||
if (widget.taskResult == null) {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final allRequestsProvider = Provider.of<AllRequestsProvider>(context, listen: false);
|
||||
await allRequestsProvider.getRecurrentWoById(id: widget.requestId);
|
||||
|
||||
// Map RecurrentWoData to CleanerTaskSearchResult
|
||||
if (allRequestsProvider.recurrentWoData != null) {
|
||||
final recurrentData = allRequestsProvider.recurrentWoData!;
|
||||
_taskData = CleanerTaskSearchResult(
|
||||
id: recurrentData.id,
|
||||
taskNo: recurrentData.taskNo,
|
||||
siteName: recurrentData.site?.siteName,
|
||||
buildingName: recurrentData.building?.name,
|
||||
floorName: recurrentData.floor?.name,
|
||||
departmentName: recurrentData.department?.name,
|
||||
roomName: recurrentData.room?.name,
|
||||
locationDescription: null, // Not available in RecurrentWoData
|
||||
createdDate: recurrentData.scheduleDate,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching recurrent work order: $e');
|
||||
} finally {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await _cleanerProvider.getCleanerRequestById(id: widget.requestId);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
appBar: DefaultAppBar(
|
||||
title: 'Request Details'.addTranslation,
|
||||
onBackPress: () => Navigator.pop(context),
|
||||
),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Selector<CleanerProvider, bool>(
|
||||
selector: (_, model) => model.isLoading,
|
||||
builder: (_, isLoading, __) {
|
||||
if (isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.toScreenWidth),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
18.height,
|
||||
_buildCleanerRequestDetailCard(context),
|
||||
20.height,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
top: false,
|
||||
right: false,
|
||||
left: false,
|
||||
child: FooterActionButton.footerContainer(
|
||||
context: context,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!_isQRScanned)
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
|
||||
size: 15,
|
||||
),
|
||||
8.width,
|
||||
Expanded(
|
||||
child: Text(
|
||||
'By scanning the QR code you will accept this task'.addTranslation,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppTextStyles.bodyText.copyWith(
|
||||
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
16.height,
|
||||
AppFilledButton(
|
||||
label: _isQRScanned ? 'Update' : 'Scan QR Code',
|
||||
maxWidth: true,
|
||||
buttonColor: AppColor.primary10,
|
||||
onPressed: () async {
|
||||
if (_isQRScanned) {
|
||||
// If QR is already scanned, navigate to recurrent work order view
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => RecurrentWorkOrderView(taskId: widget.requestId),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Open QR scanner
|
||||
final result = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const ScanQr(),
|
||||
),
|
||||
);
|
||||
|
||||
// If QR code was scanned successfully, check if it matches the room
|
||||
if (result != null && result is String && result.isNotEmpty) {
|
||||
// Show loading indicator
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => const AppLazyLoading(),
|
||||
);
|
||||
|
||||
// Call the API to check if QR matches the same room
|
||||
final isValid = await CleanerProvider().checkQRSameRoom(
|
||||
planRecurrentTaskId: widget.requestId,
|
||||
qrCode: result,
|
||||
);
|
||||
|
||||
// Close loading dialog
|
||||
if (mounted) Navigator.pop(context);
|
||||
|
||||
if (isValid) {
|
||||
// Update state to show "Update" button instead of navigating
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isQRScanned = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
).paddingOnly(start: 16, end: 16),
|
||||
),
|
||||
).toShadowContainer(context, padding: 0, showShadow: false, borderRadius: 0),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCleanerRequestDetailCard(BuildContext context) {
|
||||
final taskResult = _taskData;
|
||||
|
||||
if (taskResult == 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: [
|
||||
// Task Number and Date Row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Request Information',
|
||||
style: AppTextStyles.heading4.copyWith(
|
||||
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
|
||||
),
|
||||
),
|
||||
if (taskResult.createdDate != null)
|
||||
Text(
|
||||
taskResult.createdDate!.split('T').first,
|
||||
textAlign: TextAlign.end,
|
||||
style: AppTextStyles.tinyFont.copyWith(
|
||||
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
8.height,
|
||||
if (taskResult.taskNo != null && taskResult.taskNo!.isNotEmpty)
|
||||
Text(
|
||||
'Task No: ${taskResult.taskNo!}',
|
||||
style: AppTextStyles.bodyText.copyWith(
|
||||
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
|
||||
),
|
||||
),
|
||||
if (taskResult.siteName != null && taskResult.siteName!.isNotEmpty)
|
||||
Text(
|
||||
'Site: ${taskResult.siteName}',
|
||||
style: AppTextStyles.bodyText.copyWith(
|
||||
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
|
||||
),
|
||||
),
|
||||
if (taskResult.buildingName != null && taskResult.buildingName!.isNotEmpty)
|
||||
Text(
|
||||
'Building: ${taskResult.buildingName}',
|
||||
style: AppTextStyles.bodyText.copyWith(
|
||||
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
|
||||
),
|
||||
),
|
||||
if (taskResult.floorName != null && taskResult.floorName!.isNotEmpty)
|
||||
Text(
|
||||
'Floor: ${taskResult.floorName}',
|
||||
style: AppTextStyles.bodyText.copyWith(
|
||||
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
|
||||
),
|
||||
),
|
||||
if (taskResult.departmentName != null && taskResult.departmentName!.isNotEmpty)
|
||||
Text(
|
||||
'Department: ${taskResult.departmentName}',
|
||||
style: AppTextStyles.bodyText.copyWith(
|
||||
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
|
||||
),
|
||||
),
|
||||
if (taskResult.roomName != null && taskResult.roomName!.isNotEmpty)
|
||||
Text(
|
||||
'Room: ${taskResult.roomName}',
|
||||
style: AppTextStyles.bodyText.copyWith(
|
||||
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
|
||||
),
|
||||
),
|
||||
if (taskResult.locationDescription != null && taskResult.locationDescription!.isNotEmpty)
|
||||
Text(
|
||||
'Location: ${taskResult.locationDescription}',
|
||||
style: AppTextStyles.bodyText.copyWith(
|
||||
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
|
||||
),
|
||||
),
|
||||
],
|
||||
).expanded,
|
||||
],
|
||||
),
|
||||
],
|
||||
).paddingOnly(start: 16, end: 16, top: 16, bottom: 8),
|
||||
],
|
||||
).toShadowContainer(context, padding: 0);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,512 @@
|
||||
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/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/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/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';
|
||||
|
||||
class CleanerSearchFormView extends StatefulWidget {
|
||||
static const String routeName = '/cleaner_search';
|
||||
|
||||
const CleanerSearchFormView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<CleanerSearchFormView> createState() => _CleanerSearchFormViewState();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_siteFocusNode = FocusNode();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_siteController.dispose();
|
||||
_siteFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _searchSites(String query) async {
|
||||
if (query.isEmpty) {
|
||||
setState(() {
|
||||
_siteOptions = [];
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoadingSites = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final sites = await HospitalsProvider().getHospitalsListByVal(searchVal: query);
|
||||
setState(() {
|
||||
_siteOptions = sites;
|
||||
_isLoadingSites = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isLoadingSites = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadBuildings(int siteId) async {
|
||||
setState(() {
|
||||
_isLoadingBuildings = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final buildings = await FmsBuildingProvider().getBuildingsListBySiteId(siteId);
|
||||
setState(() {
|
||||
_buildingOptions = buildings;
|
||||
_isLoadingBuildings = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isLoadingBuildings = false;
|
||||
_buildingOptions = [];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadFloors(int siteId, int buildingId) async {
|
||||
setState(() {
|
||||
_isLoadingFloors = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final floors = await FmsFloorProvider().getFloorsListBySiteAndBuilding(siteId, buildingId);
|
||||
setState(() {
|
||||
_floorOptions = floors;
|
||||
_isLoadingFloors = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isLoadingFloors = false;
|
||||
_floorOptions = [];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadLocationDescriptions(int siteId, int buildingId, int floorId) async {
|
||||
setState(() {
|
||||
_isLoadingLocationDescriptions = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final response = await FmsLocationDescriptionProvider().getLocationDescriptionsList(siteId, buildingId, floorId);
|
||||
setState(() {
|
||||
_locationDescriptionOptions = response?.locationDescriptions ?? [];
|
||||
_allDeptRoomList = response?.deptRoom ?? [];
|
||||
_isLoadingLocationDescriptions = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isLoadingLocationDescriptions = false;
|
||||
_locationDescriptionOptions = [];
|
||||
_allDeptRoomList = [];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
List<DepartmentRoomCombined> _getDepartmentRoomCombinations() {
|
||||
if (selectedLocationDescription == null || _allDeptRoomList.isEmpty) return [];
|
||||
|
||||
// Filter dept room list by selected location description
|
||||
List<DepartmentRoomItem> filteredList = _allDeptRoomList.where((item) => item.locationDescriptionName == selectedLocationDescription!.name).toList();
|
||||
|
||||
// Convert to DepartmentRoomCombined
|
||||
return filteredList
|
||||
.map((item) => DepartmentRoomCombined(
|
||||
deptId: item.deptId,
|
||||
roomId: item.roomId,
|
||||
deptName: item.deptName,
|
||||
roomName: item.roomName,
|
||||
locationDescriptionName: item.locationDescriptionName,
|
||||
deptAndRoomName: item.deptAndRoomName,
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
void _autoSelectDepartmentRoom() {
|
||||
final combinations = _getDepartmentRoomCombinations();
|
||||
|
||||
if (combinations.isEmpty) {
|
||||
setState(() {
|
||||
selectedDepartmentRoom = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If only one item, auto-fill
|
||||
if (combinations.length == 1) {
|
||||
setState(() {
|
||||
selectedDepartmentRoom = combinations.first;
|
||||
});
|
||||
} else {
|
||||
// If more than one, auto-select first item
|
||||
setState(() {
|
||||
selectedDepartmentRoom = combinations.first;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: DefaultAppBar(
|
||||
title: 'Search Request',
|
||||
titleStyle: AppTextStyles.heading3.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).brightness == Brightness.dark ? AppColor.neutral30 : AppColor.neutral50,
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
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>(
|
||||
context: context,
|
||||
title: 'Building',
|
||||
initialValue: selectedBuilding,
|
||||
showShadow: false,
|
||||
showAsBottomSheet: true,
|
||||
backgroundColor: AppColor.fieldBgColor(context),
|
||||
enabled: _buildingOptions.isNotEmpty,
|
||||
loading: _isLoadingBuildings,
|
||||
staticData: _buildingOptions,
|
||||
onSelect: (value) async {
|
||||
_siteFocusNode.unfocus();
|
||||
|
||||
setState(() {
|
||||
_siteOptions = [];
|
||||
print('site options length ${_siteOptions.length}');
|
||||
selectedBuilding = value;
|
||||
selectedFloor = null;
|
||||
selectedLocationDescription = null;
|
||||
selectedDepartmentRoom = null;
|
||||
_floorOptions = [];
|
||||
_locationDescriptionOptions = [];
|
||||
_allDeptRoomList = [];
|
||||
});
|
||||
|
||||
// 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(),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
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),
|
||||
|
||||
// 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;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
).toShadowContainer(context).paddingOnly(top: 20, start: 16, end: 16),
|
||||
),
|
||||
),
|
||||
FooterActionButton.footerContainer(
|
||||
context: context,
|
||||
child: AppFilledButton(
|
||||
buttonColor: AppColor.primary10,
|
||||
label: context.translation.search,
|
||||
maxWidth: true,
|
||||
onPressed: _isSearching ? null : _onSubmit,
|
||||
loading: _isSearching,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onSubmit() async {
|
||||
// Validate that at least site is selected
|
||||
if (selectedSite == null) {
|
||||
'Please select a site'.showToast;
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
'Error searching'.showToast;
|
||||
} finally {
|
||||
setState(() {
|
||||
_isSearching = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,186 @@
|
||||
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,169 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.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';
|
||||
|
||||
class CleanerProvider extends ChangeNotifier {
|
||||
bool _isLoading = false;
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
// Search results
|
||||
List<dynamic>? _searchResults;
|
||||
List<dynamic>? get searchResults => _searchResults;
|
||||
|
||||
// State code for error handling
|
||||
int? _stateCode;
|
||||
int? get stateCode => _stateCode;
|
||||
|
||||
// TODO: Add your cleaner request model here
|
||||
// CleanerRequestModel? _currentCleanerRequest;
|
||||
// CleanerRequestModel? get currentCleanerRequest => _currentCleanerRequest;
|
||||
|
||||
Future<void> getCleanerRequestById({required int id}) async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
// TODO: Implement API call to fetch cleaner request details
|
||||
// Example:
|
||||
// final response = await CleanerApiService.getCleanerRequestById(id);
|
||||
// _currentCleanerRequest = response.data;
|
||||
|
||||
await Future.delayed(const Duration(seconds: 1)); // Simulating API call
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching cleaner request: $e');
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Search plan recurrent tasks
|
||||
Future<int> searchPlanRecurrentTasks({
|
||||
required int pageSize,
|
||||
required int pageNumber,
|
||||
int? siteId,
|
||||
int? buildingId,
|
||||
int? floorId,
|
||||
int? departmentId,
|
||||
int? roomId,
|
||||
}) async {
|
||||
if (_isLoading == true) return -2;
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
Response response;
|
||||
try {
|
||||
response = await ApiManager.instance.post(
|
||||
URLs.searchPlanRecurrentTaskListSearch,
|
||||
body: {
|
||||
"pageSize": pageSize,
|
||||
"pageNumber": pageNumber,
|
||||
"siteId": siteId,
|
||||
"buildingId": buildingId,
|
||||
"floorId": floorId,
|
||||
"departmentId": departmentId,
|
||||
"roomId": roomId,
|
||||
},
|
||||
);
|
||||
|
||||
_stateCode = response.statusCode;
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
// client's request was successfully received
|
||||
final responseData = json.decode(response.body);
|
||||
_searchResults = responseData["data"] as List<dynamic>?;
|
||||
} else {
|
||||
_searchResults = null;
|
||||
}
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
return response.statusCode;
|
||||
} catch (error) {
|
||||
debugPrint('Error searching plan recurrent tasks: $error');
|
||||
_isLoading = false;
|
||||
_stateCode = -1;
|
||||
_searchResults = null;
|
||||
notifyListeners();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Search plan recurrent tasks and return results directly without provider state
|
||||
Future<Map<String, dynamic>> searchPlanRecurrentTasksList({
|
||||
required int pageSize,
|
||||
required int pageNumber,
|
||||
int? siteId,
|
||||
int? buildingId,
|
||||
int? floorId,
|
||||
int? departmentId,
|
||||
int? roomId,
|
||||
}) async {
|
||||
Response response;
|
||||
try {
|
||||
response = await ApiManager.instance.post(
|
||||
URLs.searchPlanRecurrentTaskListSearch,
|
||||
body: {
|
||||
"pageSize": pageSize,
|
||||
"pageNumber": pageNumber,
|
||||
"siteId": siteId,
|
||||
"buildingId": buildingId,
|
||||
"floorId": floorId,
|
||||
"departmentId": departmentId,
|
||||
"roomId": roomId,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
final responseData = json.decode(response.body);
|
||||
return {
|
||||
"data": responseData["data"] as List<dynamic>? ?? [],
|
||||
"totalRows": responseData["totalRows"] as int? ?? 0,
|
||||
};
|
||||
}
|
||||
return {"data": [], "totalRows": 0};
|
||||
} catch (error) {
|
||||
debugPrint('Error searching plan recurrent tasks: $error');
|
||||
return {"data": [], "totalRows": 0};
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if QR code matches the same room for a plan recurrent task
|
||||
Future<bool> checkQRSameRoom({
|
||||
required int planRecurrentTaskId,
|
||||
required String qrCode,
|
||||
}) async {
|
||||
Response response;
|
||||
try {
|
||||
response = await ApiManager.instance.post(
|
||||
URLs.checkQRSameRoom,
|
||||
body: {
|
||||
"planRecurrentTaskId": planRecurrentTaskId,
|
||||
"qrCode": qrCode,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
final responseData = json.decode(response.body);
|
||||
// Assuming the API returns a boolean in the data field or a success flag
|
||||
return responseData["data"] == true || responseData["isSuccess"] == true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
debugPrint('Error checking QR same room: $error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
_isLoading = false;
|
||||
_searchResults = null;
|
||||
_stateCode = null;
|
||||
// _currentCleanerRequest = null;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
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/building.dart';
|
||||
|
||||
class FmsBuildingProvider extends ChangeNotifier {
|
||||
//reset provider data
|
||||
void reset() {
|
||||
_buildings = null;
|
||||
_stateCode = null;
|
||||
}
|
||||
|
||||
// state code of current request to defied error message
|
||||
int? _stateCode;
|
||||
int? get stateCode => _stateCode;
|
||||
|
||||
// contain buildings data
|
||||
List<Building>? _buildings;
|
||||
List<Building>? get buildings => _buildings;
|
||||
|
||||
// loading state
|
||||
bool _loading = false;
|
||||
bool get isLoading => _loading;
|
||||
|
||||
set isLoading(bool isLoading) {
|
||||
_loading = isLoading;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Get buildings by site ID
|
||||
Future<int> getBuildingsBySiteId(int siteId) async {
|
||||
if (_loading == true) return -2;
|
||||
_loading = true;
|
||||
notifyListeners();
|
||||
Response response;
|
||||
try {
|
||||
response = await ApiManager.instance.post(
|
||||
URLs.getCustomerBuildings,
|
||||
body: {
|
||||
"siteId": siteId,
|
||||
},
|
||||
);
|
||||
_stateCode = response.statusCode;
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
// client's request was successfully received
|
||||
List buildingsListJson = json.decode(response.body)["data"];
|
||||
_buildings = buildingsListJson.map((building) => Building.fromJson(building)).toList();
|
||||
}
|
||||
_loading = false;
|
||||
notifyListeners();
|
||||
return response.statusCode;
|
||||
} catch (error) {
|
||||
_loading = false;
|
||||
_stateCode = -1;
|
||||
notifyListeners();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get buildings list directly without provider state
|
||||
Future<List<Building>> getBuildingsListBySiteId(int siteId) async {
|
||||
Response response;
|
||||
try {
|
||||
response = await ApiManager.instance.post(
|
||||
URLs.getCustomerBuildings,
|
||||
body: {
|
||||
"siteId": siteId,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
List buildingsListJson = json.decode(response.body)["data"];
|
||||
return buildingsListJson.map((building) => Building.fromJson(building)).toList();
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,140 @@
|
||||
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,334 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:test_sa/controllers/api_routes/urls.dart';
|
||||
import 'package:test_sa/extensions/context_extension.dart';
|
||||
import 'package:test_sa/extensions/int_extensions.dart';
|
||||
import 'package:test_sa/extensions/string_extensions.dart';
|
||||
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 'package:test_sa/new_views/common_widgets/app_text_form_field.dart';
|
||||
|
||||
class CleanerChecklistWidget extends StatefulWidget {
|
||||
final List<ChecklistItem> checklistItems;
|
||||
final Function(List<ChecklistItem>)? onChecklistChanged;
|
||||
final String? capturedImageBase64;
|
||||
final Function(String?)? onImageCaptured;
|
||||
|
||||
const CleanerChecklistWidget({
|
||||
Key? key,
|
||||
required this.checklistItems,
|
||||
this.onChecklistChanged,
|
||||
this.capturedImageBase64,
|
||||
this.onImageCaptured,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<CleanerChecklistWidget> createState() => _CleanerChecklistWidgetState();
|
||||
}
|
||||
|
||||
class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Only show if checklist items are not empty
|
||||
if (widget.checklistItems.isEmpty) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Checklist'.addTranslation,
|
||||
style: AppTextStyles.heading4.copyWith(
|
||||
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
|
||||
),
|
||||
),
|
||||
16.height,
|
||||
...widget.checklistItems.map((item) => _buildChecklistItem(context, item)).toList(),
|
||||
16.height,
|
||||
_buildImageCaptureWidget(context),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChecklistItem(BuildContext context, ChecklistItem item) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Checkbox with title
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: item.isChecked,
|
||||
activeColor: AppColor.primary10,
|
||||
onChanged: (bool? value) {
|
||||
setState(() {
|
||||
item.isChecked = value ?? false;
|
||||
if (!item.isChecked) {
|
||||
item.commentController.clear();
|
||||
}
|
||||
});
|
||||
// Notify parent of changes
|
||||
widget.onChecklistChanged?.call(widget.checklistItems);
|
||||
},
|
||||
),
|
||||
8.width,
|
||||
Expanded(
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: item.title,
|
||||
style: AppTextStyles.bodyText.copyWith(
|
||||
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (item.isMandatory)
|
||||
TextSpan(
|
||||
text: ' *',
|
||||
style: AppTextStyles.bodyText.copyWith(
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Comment box - only visible when checked
|
||||
if (item.isChecked) ...[
|
||||
16.height,
|
||||
AppTextFormField(
|
||||
controller: item.commentController,
|
||||
labelText: context.translation.comment,
|
||||
backgroundColor: AppColor.fieldBgColor(context),
|
||||
showShadow: false,
|
||||
labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(
|
||||
color: context.isDark ? AppColor.white10 : AppColor.black10,
|
||||
),
|
||||
alignLabelWithHint: true,
|
||||
textInputType: TextInputType.multiline,
|
||||
onChange: (value) {
|
||||
item.comment = value;
|
||||
// Notify parent of changes
|
||||
widget.onChecklistChanged?.call(widget.checklistItems);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
).toShadowContainer(context).paddingOnly(bottom: 16);
|
||||
}
|
||||
|
||||
Widget _buildImageCaptureWidget(BuildContext context) {
|
||||
// Base64 image display and capture widget
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Captured Image'.addTranslation,
|
||||
style: AppTextStyles.heading6.copyWith(
|
||||
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
|
||||
),
|
||||
),
|
||||
8.height,
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
// Image capture logic
|
||||
final pickedFile = await ImagePicker().pickImage(source: ImageSource.camera);
|
||||
if (pickedFile != null) {
|
||||
// Convert image to Base64 with filename prefix (same format as CM request)
|
||||
final bytes = await pickedFile.readAsBytes();
|
||||
final base64Image = base64Encode(bytes);
|
||||
final fileName = pickedFile.path.split("/").last;
|
||||
final formattedImage = "$fileName|$base64Image";
|
||||
|
||||
// Update parent with captured image
|
||||
widget.onImageCaptured?.call(formattedImage);
|
||||
}
|
||||
},
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: _buildImageWidget(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
).toShadowContainer(context);
|
||||
}
|
||||
|
||||
/// Build the image widget with loading indicator for network images
|
||||
Widget _buildImageWidget(BuildContext context) {
|
||||
if (widget.capturedImageBase64 == null || widget.capturedImageBase64!.isEmpty) {
|
||||
// Show camera icon when no image
|
||||
return Container(
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.fieldBgColor(context),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.camera_alt,
|
||||
size: 48,
|
||||
color: context.isDark ? AppColor.white10 : AppColor.black10,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final imageString = widget.capturedImageBase64!;
|
||||
|
||||
// First check if it's base64 encoded (contains | separator)
|
||||
if (imageString.contains('|')) {
|
||||
// Format: "filename|base64data"
|
||||
try {
|
||||
final base64Data = imageString.split('|').last;
|
||||
return Container(
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.fieldBgColor(context),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Image.memory(
|
||||
base64Decode(base64Data),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Error decoding base64 image: $e');
|
||||
return _buildCameraIcon(context);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's a local file path
|
||||
if (_isLocalUrl(imageString)) {
|
||||
try {
|
||||
return Container(
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.fieldBgColor(context),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Image.file(
|
||||
File(imageString),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Error loading local file: $e');
|
||||
return _buildCameraIcon(context);
|
||||
}
|
||||
}
|
||||
|
||||
// If not local and not base64, it's a server filename - use NetworkImage with loading indicator
|
||||
final imageUrl = URLs.getFileUrl(imageString);
|
||||
if (imageUrl != null) {
|
||||
return Container(
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.fieldBgColor(context),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Image.network(
|
||||
imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) {
|
||||
return child;
|
||||
}
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: AppColor.primary10,
|
||||
value: loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
debugPrint('Error loading network image: $error');
|
||||
return _buildCameraIcon(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return _buildCameraIcon(context);
|
||||
}
|
||||
|
||||
/// Build camera icon widget
|
||||
Widget _buildCameraIcon(BuildContext context) {
|
||||
return Container(
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.fieldBgColor(context),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.camera_alt,
|
||||
size: 48,
|
||||
color: context.isDark ? AppColor.white10 : AppColor.black10,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Check if the URL is a local file path
|
||||
/// Same logic as multi_image_picker_item.dart
|
||||
bool _isLocalUrl(String url) {
|
||||
if (url.isEmpty) return false;
|
||||
return url.startsWith("/") || url.startsWith("file://") || (url.length > 1 && url.substring(1).startsWith(':\\'));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Note: Don't dispose controllers here as they're managed by ChecklistItem
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// Model class for checklist items
|
||||
class ChecklistItem {
|
||||
final int id;
|
||||
final String title;
|
||||
bool isChecked;
|
||||
String comment;
|
||||
final bool isMandatory;
|
||||
final TextEditingController commentController;
|
||||
|
||||
ChecklistItem({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.isChecked,
|
||||
required this.comment,
|
||||
this.isMandatory = false,
|
||||
}) : commentController = TextEditingController(text: comment) {
|
||||
// Update the controller text whenever comment changes
|
||||
commentController.text = comment;
|
||||
}
|
||||
|
||||
// Convert to JSON for API submission
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'isChecked': isChecked,
|
||||
'comment': commentController.text,
|
||||
'isMandatory': isMandatory,
|
||||
};
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
commentController.dispose();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
|
||||
Loading…
Reference in New Issue