You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cloudsolutions-atoms/lib/modules/cleaner_module/pages/one_time_task_view.dart

470 lines
17 KiB
Dart

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