- Added TaskListModel for managing daily tasks with JSON parsing. - Introduced WorkStatusResponseModel and WorkStatus for handling work status data. - Created MenuResponse and MenuItem models for dynamic menu management. - Updated routes to reflect correct naming conventions for task planning screens. - Enhanced DashboardScreen to include dynamic menu functionality and improved task statistics display. - Developed DailyProgressReportScreen for displaying daily progress reports with filtering options. - Implemented DailyTaskPlanningScreen for planning daily tasks with detailed views and actions. - Refactored left navigation bar to align with updated task planning routes.
310 lines
11 KiB
Dart
310 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:marco/controller/task_planning/report_task_controller.dart';
|
|
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
|
import 'package:marco/helpers/widgets/my_button.dart';
|
|
import 'package:marco/helpers/widgets/my_spacing.dart';
|
|
import 'package:marco/helpers/widgets/my_text.dart';
|
|
import 'package:marco/helpers/widgets/my_text_style.dart';
|
|
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
|
|
|
class ReportTaskBottomSheet extends StatefulWidget {
|
|
final Map<String, dynamic> taskData;
|
|
final VoidCallback? onReportSuccess;
|
|
|
|
const ReportTaskBottomSheet({
|
|
super.key,
|
|
required this.taskData,
|
|
this.onReportSuccess,
|
|
});
|
|
|
|
@override
|
|
State<ReportTaskBottomSheet> createState() => _ReportTaskBottomSheetState();
|
|
}
|
|
|
|
class _ReportTaskBottomSheetState extends State<ReportTaskBottomSheet>
|
|
with UIMixin {
|
|
late final ReportTaskController controller;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
controller = Get.put(
|
|
ReportTaskController(),
|
|
tag: widget.taskData['taskId'] ?? UniqueKey().toString(),
|
|
);
|
|
_preFillFormFields();
|
|
}
|
|
|
|
void _preFillFormFields() {
|
|
final data = widget.taskData;
|
|
final v = controller.basicValidator;
|
|
|
|
v.getController('assigned_date')?.text = data['assignedOn'] ?? '';
|
|
v.getController('assigned_by')?.text = data['assignedBy'] ?? '';
|
|
v.getController('work_area')?.text = data['location'] ?? '';
|
|
v.getController('activity')?.text = data['activity'] ?? '';
|
|
v.getController('team_size')?.text = data['teamSize']?.toString() ?? '';
|
|
v.getController('assigned')?.text = data['assigned'] ?? '';
|
|
v.getController('task_id')?.text = data['taskId'] ?? '';
|
|
v.getController('completed_work')?.clear();
|
|
v.getController('comment')?.clear();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Obx(() {
|
|
return BaseBottomSheet(
|
|
title: "Report Task",
|
|
isSubmitting: controller.reportStatus.value == ApiStatus.loading,
|
|
onCancel: () => Navigator.of(context).pop(),
|
|
onSubmit: _handleSubmit,
|
|
child: Form(
|
|
key: controller.basicValidator.formKey,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildRow("Assigned Date", controller.basicValidator.getController('assigned_date')?.text),
|
|
_buildRow("Assigned By", controller.basicValidator.getController('assigned_by')?.text),
|
|
_buildRow("Work Area", controller.basicValidator.getController('work_area')?.text),
|
|
_buildRow("Activity", controller.basicValidator.getController('activity')?.text),
|
|
_buildRow("Team Size", controller.basicValidator.getController('team_size')?.text),
|
|
_buildRow(
|
|
"Assigned",
|
|
"${controller.basicValidator.getController('assigned')?.text ?? '-'} "
|
|
"of ${widget.taskData['pendingWork'] ?? '-'} Pending",
|
|
),
|
|
_buildCompletedWorkField(),
|
|
_buildCommentField(),
|
|
Obx(() => _buildImageSection()),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
Future<void> _handleSubmit() async {
|
|
final v = controller.basicValidator;
|
|
|
|
if (v.validateForm()) {
|
|
final success = await controller.reportTask(
|
|
projectId: v.getController('task_id')?.text ?? '',
|
|
comment: v.getController('comment')?.text ?? '',
|
|
completedTask: int.tryParse(v.getController('completed_work')?.text ?? '') ?? 0,
|
|
checklist: [],
|
|
reportedDate: DateTime.now(),
|
|
images: controller.selectedImages,
|
|
);
|
|
|
|
if (success) {
|
|
widget.onReportSuccess?.call();
|
|
}
|
|
}
|
|
}
|
|
|
|
Widget _buildRow(String label, String? value) {
|
|
final icons = {
|
|
"Assigned Date": Icons.calendar_today_outlined,
|
|
"Assigned By": Icons.person_outline,
|
|
"Work Area": Icons.place_outlined,
|
|
"Activity": Icons.run_circle_outlined,
|
|
"Team Size": Icons.group_outlined,
|
|
"Assigned": Icons.assignment_turned_in_outlined,
|
|
};
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 16),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Icon(icons[label] ?? Icons.info_outline, size: 18, color: Colors.grey[700]),
|
|
MySpacing.width(8),
|
|
MyText.titleSmall("$label:", fontWeight: 600),
|
|
MySpacing.width(12),
|
|
Expanded(
|
|
child: MyText.bodyMedium(value?.trim().isNotEmpty == true ? value!.trim() : "-"),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCompletedWorkField() {
|
|
final pending = widget.taskData['pendingWork'] ?? 0;
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(Icons.work_outline, size: 18, color: Colors.grey[700]),
|
|
MySpacing.width(8),
|
|
MyText.titleSmall("Completed Work:", fontWeight: 600),
|
|
],
|
|
),
|
|
MySpacing.height(8),
|
|
TextFormField(
|
|
controller: controller.basicValidator.getController('completed_work'),
|
|
keyboardType: TextInputType.number,
|
|
validator: (value) {
|
|
if (value == null || value.trim().isEmpty) return 'Please enter completed work';
|
|
final completed = int.tryParse(value.trim());
|
|
if (completed == null) return 'Enter a valid number';
|
|
if (completed > pending) return 'Completed work cannot exceed pending work $pending';
|
|
return null;
|
|
},
|
|
decoration: InputDecoration(
|
|
hintText: "eg: 10",
|
|
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
|
border: outlineInputBorder,
|
|
enabledBorder: outlineInputBorder,
|
|
focusedBorder: focusedInputBorder,
|
|
contentPadding: MySpacing.all(16),
|
|
isCollapsed: true,
|
|
floatingLabelBehavior: FloatingLabelBehavior.never,
|
|
),
|
|
),
|
|
MySpacing.height(24),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildCommentField() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(Icons.comment_outlined, size: 18, color: Colors.grey[700]),
|
|
MySpacing.width(8),
|
|
MyText.titleSmall("Comment:", fontWeight: 600),
|
|
],
|
|
),
|
|
MySpacing.height(8),
|
|
TextFormField(
|
|
controller: controller.basicValidator.getController('comment'),
|
|
validator: controller.basicValidator.getValidation('comment'),
|
|
keyboardType: TextInputType.text,
|
|
decoration: InputDecoration(
|
|
hintText: "eg: Work done successfully",
|
|
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
|
border: outlineInputBorder,
|
|
enabledBorder: outlineInputBorder,
|
|
focusedBorder: focusedInputBorder,
|
|
contentPadding: MySpacing.all(16),
|
|
isCollapsed: true,
|
|
floatingLabelBehavior: FloatingLabelBehavior.never,
|
|
),
|
|
),
|
|
MySpacing.height(24),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildImageSection() {
|
|
final images = controller.selectedImages;
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(Icons.camera_alt_outlined, size: 18, color: Colors.grey[700]),
|
|
MySpacing.width(8),
|
|
MyText.titleSmall("Attach Photos:", fontWeight: 600),
|
|
],
|
|
),
|
|
MySpacing.height(12),
|
|
if (images.isEmpty)
|
|
Container(
|
|
height: 70,
|
|
width: double.infinity,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: Colors.grey.shade300, width: 2),
|
|
color: Colors.grey.shade100,
|
|
),
|
|
child: Center(
|
|
child: Icon(Icons.photo_camera_outlined, size: 48, color: Colors.grey.shade400),
|
|
),
|
|
)
|
|
else
|
|
SizedBox(
|
|
height: 70,
|
|
child: ListView.separated(
|
|
scrollDirection: Axis.horizontal,
|
|
itemCount: images.length,
|
|
separatorBuilder: (_, __) => MySpacing.width(12),
|
|
itemBuilder: (context, index) {
|
|
final file = images[index];
|
|
return Stack(
|
|
children: [
|
|
GestureDetector(
|
|
onTap: () {
|
|
showDialog(
|
|
context: context,
|
|
builder: (_) => Dialog(
|
|
child: InteractiveViewer(child: Image.file(file)),
|
|
),
|
|
);
|
|
},
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: Image.file(file, height: 70, width: 70, fit: BoxFit.cover),
|
|
),
|
|
),
|
|
Positioned(
|
|
top: 4,
|
|
right: 4,
|
|
child: GestureDetector(
|
|
onTap: () => controller.removeImageAt(index),
|
|
child: Container(
|
|
decoration: BoxDecoration(color: Colors.black54, shape: BoxShape.circle),
|
|
child: const Icon(Icons.close, size: 20, color: Colors.white),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
MySpacing.height(16),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: MyButton.outlined(
|
|
onPressed: () => controller.pickImages(fromCamera: true),
|
|
padding: MySpacing.xy(12, 10),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.camera_alt, size: 16, color: Colors.blueAccent),
|
|
MySpacing.width(6),
|
|
MyText.bodySmall('Capture', color: Colors.blueAccent),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
MySpacing.width(12),
|
|
Expanded(
|
|
child: MyButton.outlined(
|
|
onPressed: () => controller.pickImages(fromCamera: false),
|
|
padding: MySpacing.xy(12, 10),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.upload_file, size: 16, color: Colors.blueAccent),
|
|
MySpacing.width(6),
|
|
MyText.bodySmall('Upload', color: Colors.blueAccent),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
} |