- Added a floating action button to the Layout widget for better accessibility. - Updated the left bar navigation items for clarity and consistency. - Introduced Daily Progress Report and Daily Task Planning screens with comprehensive UI. - Implemented filtering and refreshing functionalities in task planning. - Improved user experience with better spacing and layout adjustments. - Updated pubspec.yaml to include new dependencies for image handling and path management.
403 lines
14 KiB
Dart
403 lines
14 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:marco/controller/task_planing/daily_task_planing_controller.dart';
|
|
import 'package:marco/helpers/widgets/my_button.dart';
|
|
import 'package:marco/helpers/widgets/my_text.dart';
|
|
import 'package:marco/helpers/widgets/my_spacing.dart';
|
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
|
|
|
class AssignTaskBottomSheet extends StatefulWidget {
|
|
final String workLocation;
|
|
final String activityName;
|
|
final int pendingTask;
|
|
final String workItemId;
|
|
final DateTime assignmentDate;
|
|
final String buildingName;
|
|
final String floorName;
|
|
final String workAreaName;
|
|
|
|
const AssignTaskBottomSheet({
|
|
super.key,
|
|
required this.buildingName,
|
|
required this.workLocation,
|
|
required this.floorName,
|
|
required this.workAreaName,
|
|
required this.activityName,
|
|
required this.pendingTask,
|
|
required this.workItemId,
|
|
required this.assignmentDate,
|
|
});
|
|
|
|
@override
|
|
State<AssignTaskBottomSheet> createState() => _AssignTaskBottomSheetState();
|
|
}
|
|
|
|
class _AssignTaskBottomSheetState extends State<AssignTaskBottomSheet> {
|
|
final DailyTaskPlaningController controller = Get.find();
|
|
final TextEditingController targetController = TextEditingController();
|
|
final TextEditingController descriptionController = TextEditingController();
|
|
String? selectedProjectId;
|
|
|
|
final ScrollController _employeeListScrollController = ScrollController();
|
|
|
|
@override
|
|
void dispose() {
|
|
_employeeListScrollController.dispose();
|
|
targetController.dispose();
|
|
descriptionController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
selectedProjectId = controller.selectedProjectId;
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (selectedProjectId != null) {
|
|
controller.fetchEmployeesByProject(selectedProjectId!);
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SafeArea(
|
|
child: Container(
|
|
padding: MediaQuery.of(context).viewInsets.add(MySpacing.all(16)),
|
|
decoration: const BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
|
),
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(Icons.assignment, color: Colors.black54),
|
|
SizedBox(width: 8),
|
|
MyText.titleMedium("Assign Task",
|
|
fontSize: 18, fontWeight: 600),
|
|
],
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.close),
|
|
onPressed: () => Get.back(),
|
|
),
|
|
],
|
|
),
|
|
Divider(),
|
|
_infoRow(Icons.location_on, "Work Location",
|
|
"${widget.buildingName} > ${widget.floorName} > ${widget.workAreaName} > ${widget.activityName}"),
|
|
Divider(),
|
|
_infoRow(Icons.pending_actions, "Pending Task of Activity",
|
|
"${widget.pendingTask}"),
|
|
Divider(),
|
|
GestureDetector(
|
|
onTap: () {
|
|
final RenderBox overlay = Overlay.of(context)
|
|
.context
|
|
.findRenderObject() as RenderBox;
|
|
final Size screenSize = overlay.size;
|
|
|
|
showMenu(
|
|
context: context,
|
|
position: RelativeRect.fromLTRB(
|
|
screenSize.width / 2 - 100,
|
|
screenSize.height / 2 - 20,
|
|
screenSize.width / 2 - 100,
|
|
screenSize.height / 2 - 20,
|
|
),
|
|
items: [
|
|
const PopupMenuItem(
|
|
value: 'all',
|
|
child: Text("All Roles"),
|
|
),
|
|
...controller.roles.map((role) {
|
|
return PopupMenuItem(
|
|
value: role['id'].toString(),
|
|
child: Text(role['name'] ?? 'Unknown Role'),
|
|
);
|
|
}),
|
|
],
|
|
).then((value) {
|
|
if (value != null) {
|
|
controller.onRoleSelected(value == 'all' ? null : value);
|
|
}
|
|
});
|
|
},
|
|
child: Row(
|
|
children: [
|
|
MyText.titleMedium("Select Team :", fontWeight: 600),
|
|
const SizedBox(width: 4),
|
|
Icon(Icons.filter_alt,
|
|
color: const Color.fromARGB(255, 95, 132, 255)),
|
|
],
|
|
),
|
|
),
|
|
MySpacing.height(8),
|
|
Container(
|
|
constraints: BoxConstraints(
|
|
maxHeight: 150,
|
|
),
|
|
child: _buildEmployeeList(),
|
|
),
|
|
MySpacing.height(8),
|
|
Obx(() {
|
|
if (controller.selectedEmployees.isEmpty) {
|
|
return Container();
|
|
}
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
child: Wrap(
|
|
spacing: 4,
|
|
runSpacing: 4,
|
|
children: controller.selectedEmployees.map((e) {
|
|
return Obx(() {
|
|
final isSelected =
|
|
controller.uploadingStates[e.id]?.value ?? false;
|
|
if (!isSelected) return Container();
|
|
|
|
return Chip(
|
|
label: Text(e.name,
|
|
style: const TextStyle(color: Colors.white)),
|
|
backgroundColor:
|
|
const Color.fromARGB(255, 95, 132, 255),
|
|
deleteIcon:
|
|
const Icon(Icons.close, color: Colors.white),
|
|
onDeleted: () {
|
|
controller.uploadingStates[e.id]?.value = false;
|
|
controller.updateSelectedEmployees();
|
|
});
|
|
});
|
|
}).toList(),
|
|
),
|
|
);
|
|
}),
|
|
_buildTextField(
|
|
icon: Icons.track_changes,
|
|
label: "Target for Today :",
|
|
controller: targetController,
|
|
hintText: "Enter target",
|
|
keyboardType: TextInputType.number,
|
|
validatorType: "target",
|
|
),
|
|
MySpacing.height(24),
|
|
_buildTextField(
|
|
icon: Icons.description,
|
|
label: "Description :",
|
|
controller: descriptionController,
|
|
hintText: "Enter task description",
|
|
maxLines: 3,
|
|
validatorType: "description",
|
|
),
|
|
MySpacing.height(24),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
MyButton(
|
|
onPressed: _onAssignTaskPressed,
|
|
backgroundColor: const Color.fromARGB(255, 95, 132, 255),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
MyText.bodyMedium("Assign Task", color: Colors.white),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildEmployeeList() {
|
|
return Obx(() {
|
|
if (controller.isLoading.value) {
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
|
|
final selectedRoleId = controller.selectedRoleId.value;
|
|
|
|
final filteredEmployees = selectedRoleId == null
|
|
? controller.employees
|
|
: controller.employees
|
|
.where((e) => e.jobRoleID.toString() == selectedRoleId)
|
|
.toList();
|
|
|
|
if (filteredEmployees.isEmpty) {
|
|
return const Text("No employees found for selected role.");
|
|
}
|
|
|
|
return Scrollbar(
|
|
controller: _employeeListScrollController,
|
|
thumbVisibility: true,
|
|
interactive: true,
|
|
child: ListView.builder(
|
|
controller: _employeeListScrollController,
|
|
shrinkWrap: true,
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
itemCount: filteredEmployees.length,
|
|
itemBuilder: (context, index) {
|
|
final employee = filteredEmployees[index];
|
|
final rxBool = controller.uploadingStates[employee.id];
|
|
return Obx(() => Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 0),
|
|
child: Row(
|
|
children: [
|
|
Theme(
|
|
data: Theme.of(context)
|
|
.copyWith(unselectedWidgetColor: Colors.black),
|
|
child: Checkbox(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(4),
|
|
side: const BorderSide(color: Colors.black),
|
|
),
|
|
value: rxBool?.value ?? false,
|
|
onChanged: (bool? selected) {
|
|
if (rxBool != null) {
|
|
rxBool.value = selected ?? false;
|
|
controller.updateSelectedEmployees();
|
|
}
|
|
},
|
|
fillColor:
|
|
WidgetStateProperty.resolveWith<Color>((states) {
|
|
if (states.contains(WidgetState.selected)) {
|
|
return const Color.fromARGB(255, 95, 132, 255);
|
|
}
|
|
return Colors.transparent;
|
|
}),
|
|
checkColor: Colors.white,
|
|
side: const BorderSide(color: Colors.black),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(employee.name,
|
|
style: TextStyle(fontSize: 14))),
|
|
],
|
|
),
|
|
));
|
|
},
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
Widget _buildTextField({
|
|
required IconData icon,
|
|
required String label,
|
|
required TextEditingController controller,
|
|
required String hintText,
|
|
TextInputType keyboardType = TextInputType.text,
|
|
int maxLines = 1,
|
|
required String validatorType,
|
|
}) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(icon, size: 18, color: Colors.black54),
|
|
const SizedBox(width: 6),
|
|
MyText.titleMedium(label, fontWeight: 600),
|
|
],
|
|
),
|
|
MySpacing.height(6),
|
|
TextFormField(
|
|
controller: controller,
|
|
keyboardType: keyboardType,
|
|
maxLines: maxLines,
|
|
decoration: InputDecoration(
|
|
hintText: hintText,
|
|
border: const OutlineInputBorder(),
|
|
),
|
|
validator: (value) => this
|
|
.controller
|
|
.formFieldValidator(value, fieldType: validatorType),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _infoRow(IconData icon, String title, String value) {
|
|
return Padding(
|
|
padding: MySpacing.y(6),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Icon(icon, size: 20, color: Colors.grey[700]),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: RichText(
|
|
text: TextSpan(
|
|
children: [
|
|
WidgetSpan(
|
|
child: MyText.titleMedium("$title: ",
|
|
fontWeight: 600, color: Colors.black),
|
|
),
|
|
TextSpan(
|
|
text: value,
|
|
style: const TextStyle(color: Colors.black),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _onAssignTaskPressed() {
|
|
final selectedTeam = controller.uploadingStates.entries
|
|
.where((e) => e.value.value)
|
|
.map((e) => e.key)
|
|
.toList();
|
|
|
|
if (selectedTeam.isEmpty) {
|
|
showAppSnackbar(
|
|
title: "Team Required",
|
|
message: "Please select at least one team member",
|
|
type: SnackbarType.error,
|
|
);
|
|
return;
|
|
}
|
|
|
|
final target = int.tryParse(targetController.text.trim());
|
|
if (target == null || target <= 0) {
|
|
showAppSnackbar(
|
|
title: "Invalid Input",
|
|
message: "Please enter a valid target number",
|
|
type: SnackbarType.error,
|
|
);
|
|
return;
|
|
}
|
|
|
|
final description = descriptionController.text.trim();
|
|
if (description.isEmpty) {
|
|
showAppSnackbar(
|
|
title: "Description Required",
|
|
message: "Please enter a description",
|
|
type: SnackbarType.error,
|
|
);
|
|
return;
|
|
}
|
|
|
|
controller.assignDailyTask(
|
|
workItemId: widget.workItemId,
|
|
plannedTask: target,
|
|
description: description,
|
|
taskTeam: selectedTeam,
|
|
assignmentDate: widget.assignmentDate,
|
|
);
|
|
}
|
|
}
|