cleaner module implementation new flow complete
parent
5c46fdd8e5
commit
c35162fd88
@ -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,
|
||||
});
|
||||
}
|
||||
@ -1,69 +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/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 '../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!.toServiceRequestCardFormat,
|
||||
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,183 +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: null),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,373 @@
|
||||
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 '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) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Working Hours',
|
||||
style: AppTextStyles.heading5.copyWith(
|
||||
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Total Working Hours',
|
||||
style: AppTextStyles.bodyText.copyWith(
|
||||
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${totalWorkingHours.toStringAsFixed(2)} hrs',
|
||||
style: AppTextStyles.heading6.copyWith(
|
||||
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingAll(12),
|
||||
],
|
||||
).toShadowContainer(context, padding: 12);
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,83 +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/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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,457 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.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/widget_extensions.dart';
|
||||
import 'package:test_sa/extensions/text_extensions.dart';
|
||||
import 'package:test_sa/models/ppm/recurrent_wo.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/views/widgets/buttons/app_back_button.dart';
|
||||
import 'package:test_sa/modules/cm_module/cm_request_utils.dart';
|
||||
import 'package:test_sa/views/widgets/total_working_time_detail_bottomsheet.dart';
|
||||
|
||||
class CleanerPreviewDialog extends StatelessWidget {
|
||||
final RecurrentWoData model;
|
||||
final double totalWorkingHours;
|
||||
final VoidCallback onConfirm;
|
||||
|
||||
const CleanerPreviewDialog({
|
||||
Key? key,
|
||||
required this.model,
|
||||
required this.totalWorkingHours,
|
||||
required this.onConfirm,
|
||||
}) : super(key: key);
|
||||
|
||||
static void show(
|
||||
BuildContext context, {
|
||||
required RecurrentWoData model,
|
||||
required double totalWorkingHours,
|
||||
required VoidCallback onConfirm,
|
||||
}) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) => CleanerPreviewDialog(
|
||||
model: model,
|
||||
totalWorkingHours: totalWorkingHours,
|
||||
onConfirm: onConfirm,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.85),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header matching DefaultAppBar style
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(14),
|
||||
topRight: Radius.circular(14),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: 'Confirm Details'.heading3(context).custom(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.close,
|
||||
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
|
||||
size: 24,
|
||||
).onPress(() => Navigator.pop(context)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(height: 1, color: context.isDark ? AppColor.neutral60 : AppColor.neutral100),
|
||||
// Content
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildPreviewSection(context, 'Task Information', [
|
||||
_buildPreviewRow(context, context.translation.title, model.title ?? '-'),
|
||||
_buildPreviewRow(context, context.translation.taskNo, model.taskNo ?? '-'),
|
||||
_buildPreviewRow(context, context.translation.site, model.site?.siteName ?? '-'),
|
||||
_buildPreviewRow(context, context.translation.department, model.department?.name ?? '-'),
|
||||
_buildPreviewRow(context, context.translation.floor, model.floor?.name ?? '-'),
|
||||
_buildPreviewRow(context, context.translation.room, model.room?.name ?? '-'),
|
||||
_buildPreviewRow(context, context.translation.assignedEmployee, model.engineer?.userName ?? '-'),
|
||||
]),
|
||||
16.height,
|
||||
// Total Working Time Section - Using WorkingTimeTile design
|
||||
_buildPreviewSection(context, 'Total Working Time', [
|
||||
_buildWorkingTimeTile(context),
|
||||
]),
|
||||
16.height,
|
||||
if (model.planRecurrentTaskCheckLists != null && model.planRecurrentTaskCheckLists!.isNotEmpty) ...[
|
||||
_buildPreviewSection(context, 'Checklist', [
|
||||
...model.planRecurrentTaskCheckLists!
|
||||
.map(
|
||||
(item) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
item.isChecked ? Icons.check_circle : Icons.circle_outlined,
|
||||
color: item.isChecked ? AppColor.primary10 : AppColor.neutral120,
|
||||
size: 20,
|
||||
),
|
||||
8.width,
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: item.title,
|
||||
style: AppTextStyles.bodyText2.copyWith(
|
||||
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
|
||||
),
|
||||
),
|
||||
if (item.isMandatory)
|
||||
TextSpan(
|
||||
text: ' *',
|
||||
style: AppTextStyles.bodyText2.copyWith(
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
).expanded,
|
||||
],
|
||||
),
|
||||
// Show comment if item is checked and has a comment
|
||||
if (item.isChecked && item.comment.isNotEmpty == true) ...[
|
||||
8.height,
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(8),
|
||||
margin: const EdgeInsets.only(left: 28),
|
||||
decoration: BoxDecoration(
|
||||
color: context.isDark ? AppColor.neutral60.withValues(alpha: 0.3) : AppColor.neutral100.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
'Comment:'.bodyText2(context).custom(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.isDark ? AppColor.neutral30 : AppColor.neutral120,
|
||||
),
|
||||
4.height,
|
||||
item.comment.bodyText2(context).custom(
|
||||
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
]),
|
||||
16.height,
|
||||
],
|
||||
if (model.comment != null && model.comment!.isNotEmpty) ...[
|
||||
_buildPreviewSection(context, context.translation.comment, [
|
||||
model.comment!.bodyText(context),
|
||||
]),
|
||||
16.height,
|
||||
],
|
||||
if (model.imageRoom != null && model.imageRoom!.isNotEmpty) ...[
|
||||
_buildPreviewSection(context, 'Captured Image', [
|
||||
_buildImagePreview(context, model.imageRoom!),
|
||||
]),
|
||||
16.height,
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Footer with Confirm button
|
||||
Container(
|
||||
padding: EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
border: Border(top: BorderSide(color: context.isDark ? AppColor.neutral60 : AppColor.neutral100)),
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(14),
|
||||
bottomRight: Radius.circular(14),
|
||||
),
|
||||
),
|
||||
child: AppFilledButton(
|
||||
label: context.translation.complete,
|
||||
buttonColor: AppColor.primary10,
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
onConfirm();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPreviewSection(BuildContext context, String title, List<Widget> children) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
title.heading6(context).custom(fontWeight: FontWeight.w600),
|
||||
8.height,
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: children,
|
||||
).toShadowContainer(context, padding: 12),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPreviewRow(BuildContext context, String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: '$label:'.bodyText2(context).custom(color: AppColor.neutral120),
|
||||
),
|
||||
8.width,
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: value.bodyText2(context).custom(color: AppColor.neutral120),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWorkingTimeTile(BuildContext context) {
|
||||
// Create timer list from model data, same as in RecurrentTaskInfoWidget
|
||||
final timerList = model.planRecurrentTaskTimers?.map((e) {
|
||||
return TimerHistoryModel(
|
||||
id: e.id,
|
||||
startTime: e.startTime,
|
||||
endTime: e.endTime,
|
||||
workingHours: e.workingHours,
|
||||
);
|
||||
}).toList() ?? [];
|
||||
|
||||
return Container(
|
||||
height: 56.toScreenHeight,
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.toScreenWidth),
|
||||
alignment: Alignment.centerLeft,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.fieldBgColor(context),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"Total Working Time",
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: context.isDark ? null : AppColor.neutral20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
" ${CMRequestUtils.formatTotalWorkingHours(totalWorkingHours)}",
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
).expanded,
|
||||
8.width,
|
||||
const Icon(Icons.info_outline_rounded, color: AppColor.primary10),
|
||||
],
|
||||
),
|
||||
).onPress(() async {
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
isDismissible: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
builder: (BuildContext context) => TotalWorkingTimeDetailBottomSheet(timerList: timerList),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildImagePreview(BuildContext context, String imageString) {
|
||||
Widget imageWidget;
|
||||
|
||||
// First check if it's base64 encoded (contains | separator)
|
||||
if (imageString.contains('|')) {
|
||||
// Format: "filename|base64data"
|
||||
try {
|
||||
final base64Data = imageString.split('|').last;
|
||||
imageWidget = Image.memory(base64Decode(base64Data), fit: BoxFit.cover);
|
||||
return Container(
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.fieldBgColor(context),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: imageWidget,
|
||||
).onPress(() {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => Scaffold(
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
InteractiveViewer(child: imageWidget).center,
|
||||
const ABackButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('Error decoding base64 image: $e');
|
||||
return _buildCameraIcon(context);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's a local file path
|
||||
if (_isLocalUrl(imageString)) {
|
||||
try {
|
||||
imageWidget = Image.file(File(imageString), fit: BoxFit.cover);
|
||||
return Container(
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.fieldBgColor(context),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: imageWidget,
|
||||
).onPress(() {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => Scaffold(
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
InteractiveViewer(child: imageWidget).center,
|
||||
const ABackButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
} 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) {
|
||||
imageWidget = Image.network(
|
||||
imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) {
|
||||
return child;
|
||||
}
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: AppColor.primary10,
|
||||
value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null,
|
||||
),
|
||||
);
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
debugPrint('Error loading network image: $error');
|
||||
return _buildCameraIcon(context);
|
||||
},
|
||||
);
|
||||
return Container(
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.fieldBgColor(context),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: imageWidget,
|
||||
).onPress(() {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => Scaffold(
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
InteractiveViewer(child: imageWidget).center,
|
||||
const ABackButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
return _buildCameraIcon(context);
|
||||
}
|
||||
|
||||
/// Build camera icon widget for fallback
|
||||
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
|
||||
bool _isLocalUrl(String url) {
|
||||
return url.startsWith('/') || url.startsWith('file://') || (url.length > 1 && url[1] == ':');
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue