Add job status management to service project details screen
This commit is contained in:
parent
341d779499
commit
7bef2e9d89
@ -6,6 +6,7 @@ import 'package:on_field_work/model/service_project/service_project_job_detail_m
|
|||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:on_field_work/model/service_project/job_attendance_logs_model.dart';
|
import 'package:on_field_work/model/service_project/job_attendance_logs_model.dart';
|
||||||
import 'package:on_field_work/model/service_project/job_allocation_model.dart';
|
import 'package:on_field_work/model/service_project/job_allocation_model.dart';
|
||||||
|
import 'package:on_field_work/model/service_project/job_status_response.dart';
|
||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
@ -41,6 +42,12 @@ class ServiceProjectDetailsController extends GetxController {
|
|||||||
var isTeamLoading = false.obs;
|
var isTeamLoading = false.obs;
|
||||||
var teamErrorMessage = ''.obs;
|
var teamErrorMessage = ''.obs;
|
||||||
var filteredJobList = <JobEntity>[].obs;
|
var filteredJobList = <JobEntity>[].obs;
|
||||||
|
// -------------------- Job Status --------------------
|
||||||
|
// With this:
|
||||||
|
var jobStatusList = <JobStatus>[].obs;
|
||||||
|
var selectedJobStatus = Rx<JobStatus?>(null);
|
||||||
|
var isJobStatusLoading = false.obs;
|
||||||
|
var jobStatusErrorMessage = ''.obs;
|
||||||
|
|
||||||
// -------------------- Lifecycle --------------------
|
// -------------------- Lifecycle --------------------
|
||||||
@override
|
@override
|
||||||
@ -110,6 +117,41 @@ class ServiceProjectDetailsController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> fetchJobStatus({required String statusId}) async {
|
||||||
|
if (projectId.value.isEmpty) {
|
||||||
|
jobStatusErrorMessage.value = "Invalid project ID";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isJobStatusLoading.value = true;
|
||||||
|
jobStatusErrorMessage.value = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
final statuses = await ApiService.getMasterJobStatus(
|
||||||
|
projectId: projectId.value,
|
||||||
|
statusId: statusId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (statuses != null && statuses.isNotEmpty) {
|
||||||
|
jobStatusList.value = statuses;
|
||||||
|
|
||||||
|
// Keep previously selected if exists, else pick first
|
||||||
|
selectedJobStatus.value = statuses.firstWhere(
|
||||||
|
(status) => status.id == selectedJobStatus.value?.id,
|
||||||
|
orElse: () => statuses.first,
|
||||||
|
);
|
||||||
|
|
||||||
|
print("Job Status List: ${jobStatusList.map((e) => e.name).toList()}");
|
||||||
|
} else {
|
||||||
|
jobStatusErrorMessage.value = "No job statuses found";
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
jobStatusErrorMessage.value = "Error fetching job status: $e";
|
||||||
|
} finally {
|
||||||
|
isJobStatusLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> fetchProjectDetail() async {
|
Future<void> fetchProjectDetail() async {
|
||||||
if (projectId.value.isEmpty) {
|
if (projectId.value.isEmpty) {
|
||||||
errorMessage.value = "Invalid project ID";
|
errorMessage.value = "Invalid project ID";
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
class ApiEndpoints {
|
class ApiEndpoints {
|
||||||
// static const String baseUrl = "https://stageapi.marcoaiot.com/api";
|
static const String baseUrl = "https://stageapi.marcoaiot.com/api";
|
||||||
// static const String baseUrl = "https://api.marcoaiot.com/api";
|
// static const String baseUrl = "https://api.marcoaiot.com/api";
|
||||||
// static const String baseUrl = "https://devapi.marcoaiot.com/api";
|
// static const String baseUrl = "https://devapi.marcoaiot.com/api";
|
||||||
// static const String baseUrl = "https://mapi.marcoaiot.com/api";
|
// static const String baseUrl = "https://mapi.marcoaiot.com/api";
|
||||||
static const String baseUrl = "https://api.onfieldwork.com/api";
|
// static const String baseUrl = "https://api.onfieldwork.com/api";
|
||||||
|
|
||||||
|
|
||||||
static const String getMasterCurrencies = "/Master/currencies/list";
|
static const String getMasterCurrencies = "/Master/currencies/list";
|
||||||
@ -152,4 +152,6 @@ class ApiEndpoints {
|
|||||||
static const String manageServiceProjectUpateJobAllocation = "/serviceproject/manage/allocation";
|
static const String manageServiceProjectUpateJobAllocation = "/serviceproject/manage/allocation";
|
||||||
static const String getTeamRoles = "/master/team-roles/list";
|
static const String getTeamRoles = "/master/team-roles/list";
|
||||||
static const String getServiceProjectBranches = "/serviceproject/branch/list";
|
static const String getServiceProjectBranches = "/serviceproject/branch/list";
|
||||||
|
|
||||||
|
static const String getMasterJobStatus = "/Master/job-status/list";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,6 +40,7 @@ import 'package:on_field_work/model/service_project/service_project_job_detail_m
|
|||||||
import 'package:on_field_work/model/service_project/job_attendance_logs_model.dart';
|
import 'package:on_field_work/model/service_project/job_attendance_logs_model.dart';
|
||||||
import 'package:on_field_work/model/service_project/job_allocation_model.dart';
|
import 'package:on_field_work/model/service_project/job_allocation_model.dart';
|
||||||
import 'package:on_field_work/model/service_project/service_project_branches_model.dart';
|
import 'package:on_field_work/model/service_project/service_project_branches_model.dart';
|
||||||
|
import 'package:on_field_work/model/service_project/job_status_response.dart';
|
||||||
|
|
||||||
class ApiService {
|
class ApiService {
|
||||||
static const bool enableLogs = true;
|
static const bool enableLogs = true;
|
||||||
@ -311,6 +312,44 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<List<JobStatus>?> getMasterJobStatus({
|
||||||
|
required String statusId,
|
||||||
|
required String projectId,
|
||||||
|
}) async {
|
||||||
|
final queryParams = {
|
||||||
|
'statusId': statusId,
|
||||||
|
'projectId': projectId,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await _getRequest(
|
||||||
|
ApiEndpoints.getMasterJobStatus,
|
||||||
|
queryParams: queryParams,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
_log("getMasterJobStatus: No response received.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final parsedJson =
|
||||||
|
_parseResponseForAllData(response, label: "MasterJobStatus");
|
||||||
|
|
||||||
|
if (parsedJson == null) return null;
|
||||||
|
|
||||||
|
// Directly parse JobStatus list
|
||||||
|
final dataList = (parsedJson['data'] as List<dynamic>?)
|
||||||
|
?.map((e) => JobStatus.fromJson(e))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return dataList;
|
||||||
|
} catch (e, stack) {
|
||||||
|
_log("Exception in getMasterJobStatus: $e\n$stack",
|
||||||
|
level: LogLevel.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Fetch Service Project Branches with full response
|
/// Fetch Service Project Branches with full response
|
||||||
static Future<ServiceProjectBranchesResponse?> getServiceProjectBranchesFull({
|
static Future<ServiceProjectBranchesResponse?> getServiceProjectBranchesFull({
|
||||||
required String projectId,
|
required String projectId,
|
||||||
|
|||||||
@ -36,9 +36,8 @@ class CustomAppBar extends StatelessWidget
|
|||||||
elevation: 0,
|
elevation: 0,
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
titleSpacing: 0,
|
titleSpacing: 0,
|
||||||
shape: const RoundedRectangleBorder(
|
shadowColor: Colors.transparent,
|
||||||
borderRadius: BorderRadius.vertical(bottom: Radius.circular(0)),
|
|
||||||
),
|
|
||||||
leading: Padding(
|
leading: Padding(
|
||||||
padding: MySpacing.only(left: horizontalPadding),
|
padding: MySpacing.only(left: horizontalPadding),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
|
|||||||
85
lib/model/service_project/job_status_response.dart
Normal file
85
lib/model/service_project/job_status_response.dart
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
class JobStatusResponse {
|
||||||
|
final bool? success;
|
||||||
|
final String? message;
|
||||||
|
final List<JobStatus>? data;
|
||||||
|
final dynamic errors;
|
||||||
|
final int? statusCode;
|
||||||
|
final String? timestamp;
|
||||||
|
|
||||||
|
JobStatusResponse({
|
||||||
|
this.success,
|
||||||
|
this.message,
|
||||||
|
this.data,
|
||||||
|
this.errors,
|
||||||
|
this.statusCode,
|
||||||
|
this.timestamp,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory JobStatusResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
return JobStatusResponse(
|
||||||
|
success: json['success'] as bool?,
|
||||||
|
message: json['message'] as String?,
|
||||||
|
data: (json['data'] as List<dynamic>?)
|
||||||
|
?.map((e) => JobStatus.fromJson(e))
|
||||||
|
.toList(),
|
||||||
|
errors: json['errors'],
|
||||||
|
statusCode: json['statusCode'] as int?,
|
||||||
|
timestamp: json['timestamp'] as String?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'success': success,
|
||||||
|
'message': message,
|
||||||
|
'data': data?.map((e) => e.toJson()).toList(),
|
||||||
|
'errors': errors,
|
||||||
|
'statusCode': statusCode,
|
||||||
|
'timestamp': timestamp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------
|
||||||
|
// Single Job Status Model
|
||||||
|
// --------------------------
|
||||||
|
class JobStatus {
|
||||||
|
final String? id;
|
||||||
|
final String? name;
|
||||||
|
final String? displayName;
|
||||||
|
final int? level;
|
||||||
|
|
||||||
|
JobStatus({
|
||||||
|
this.id,
|
||||||
|
this.name,
|
||||||
|
this.displayName,
|
||||||
|
this.level,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory JobStatus.fromJson(Map<String, dynamic> json) {
|
||||||
|
return JobStatus(
|
||||||
|
id: json['id'] as String?,
|
||||||
|
name: json['name'] as String?,
|
||||||
|
displayName: json['displayName'] as String?,
|
||||||
|
level: json['level'] as int?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'displayName': displayName,
|
||||||
|
'level': level,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Add equality by id
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is JobStatus && runtimeType == other.runtimeType && id == other.id;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => id.hashCode;
|
||||||
|
}
|
||||||
@ -18,6 +18,7 @@ import 'dart:io';
|
|||||||
import 'package:on_field_work/helpers/widgets/my_confirmation_dialog.dart';
|
import 'package:on_field_work/helpers/widgets/my_confirmation_dialog.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
|
import 'package:on_field_work/model/service_project/job_status_response.dart';
|
||||||
|
|
||||||
class JobDetailsScreen extends StatefulWidget {
|
class JobDetailsScreen extends StatefulWidget {
|
||||||
final String jobId;
|
final String jobId;
|
||||||
@ -48,10 +49,12 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
controller = Get.find<ServiceProjectDetailsController>();
|
controller = Get.find<ServiceProjectDetailsController>();
|
||||||
// fetch and seed local selected lists
|
|
||||||
controller.fetchJobDetail(widget.jobId).then((_) {
|
// Fetch job detail first
|
||||||
|
controller.fetchJobDetail(widget.jobId).then((_) async {
|
||||||
final job = controller.jobDetail.value?.data;
|
final job = controller.jobDetail.value?.data;
|
||||||
if (job != null) {
|
if (job != null) {
|
||||||
|
// Populate form fields
|
||||||
_selectedTags.value =
|
_selectedTags.value =
|
||||||
(job.tags ?? []).map((t) => Tag(id: t.id, name: t.name)).toList();
|
(job.tags ?? []).map((t) => Tag(id: t.id, name: t.name)).toList();
|
||||||
_titleController.text = job.title ?? '';
|
_titleController.text = job.title ?? '';
|
||||||
@ -63,6 +66,21 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
|||||||
job.dueDate ?? '',
|
job.dueDate ?? '',
|
||||||
format: "yyyy-MM-dd");
|
format: "yyyy-MM-dd");
|
||||||
_selectedAssignees.value = job.assignees ?? [];
|
_selectedAssignees.value = job.assignees ?? [];
|
||||||
|
|
||||||
|
// 🔹 Fetch job status only if existing status ID present
|
||||||
|
final existingStatusId = job.status?.id;
|
||||||
|
if (existingStatusId != null) {
|
||||||
|
await controller.fetchJobStatus(statusId: existingStatusId);
|
||||||
|
|
||||||
|
// Set selectedJobStatus to match existing status ID
|
||||||
|
if (controller.jobStatusList.isNotEmpty) {
|
||||||
|
controller.selectedJobStatus.value =
|
||||||
|
controller.jobStatusList.firstWhere(
|
||||||
|
(s) => s.id == existingStatusId,
|
||||||
|
orElse: () => controller.jobStatusList.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -88,18 +106,20 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _editJob() async {
|
Future<void> _editJob() async {
|
||||||
_processTagsInput();
|
_processTagsInput(); // process any new tag input
|
||||||
final job = controller.jobDetail.value?.data;
|
final job = controller.jobDetail.value?.data;
|
||||||
if (job == null) return;
|
if (job == null) return;
|
||||||
|
|
||||||
final List<Map<String, dynamic>> operations = [];
|
final List<Map<String, dynamic>> operations = [];
|
||||||
|
|
||||||
|
// 1️⃣ Title
|
||||||
final trimmedTitle = _titleController.text.trim();
|
final trimmedTitle = _titleController.text.trim();
|
||||||
if (trimmedTitle != job.title) {
|
if (trimmedTitle != job.title) {
|
||||||
operations
|
operations
|
||||||
.add({"op": "replace", "path": "/title", "value": trimmedTitle});
|
.add({"op": "replace", "path": "/title", "value": trimmedTitle});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2️⃣ Description
|
||||||
final trimmedDescription = _descriptionController.text.trim();
|
final trimmedDescription = _descriptionController.text.trim();
|
||||||
if (trimmedDescription != job.description) {
|
if (trimmedDescription != job.description) {
|
||||||
operations.add({
|
operations.add({
|
||||||
@ -109,6 +129,7 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3️⃣ Start & Due Date
|
||||||
final startDate = DateTime.tryParse(_startDateController.text);
|
final startDate = DateTime.tryParse(_startDateController.text);
|
||||||
final dueDate = DateTime.tryParse(_dueDateController.text);
|
final dueDate = DateTime.tryParse(_dueDateController.text);
|
||||||
|
|
||||||
@ -128,32 +149,27 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assignees payload (keep same approach)
|
// 4️⃣ Assignees
|
||||||
final originalAssignees = job.assignees ?? [];
|
final originalAssignees = job.assignees ?? [];
|
||||||
final assigneesPayload = originalAssignees.map((a) {
|
final assigneesPayload = originalAssignees.map((a) {
|
||||||
final isSelected = _selectedAssignees.any((s) => s.id == a.id);
|
final isSelected = _selectedAssignees.any((s) => s.id == a.id);
|
||||||
return {"employeeId": a.id, "isActive": isSelected};
|
return {"employeeId": a.id, "isActive": isSelected};
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
// add newly added assignees
|
|
||||||
for (var s in _selectedAssignees) {
|
for (var s in _selectedAssignees) {
|
||||||
if (!(originalAssignees.any((a) => a.id == s.id))) {
|
if (!originalAssignees.any((a) => a.id == s.id)) {
|
||||||
assigneesPayload.add({"employeeId": s.id, "isActive": true});
|
assigneesPayload.add({"employeeId": s.id, "isActive": true});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
operations.add(
|
operations.add(
|
||||||
{"op": "replace", "path": "/assignees", "value": assigneesPayload});
|
{"op": "replace", "path": "/assignees", "value": assigneesPayload});
|
||||||
|
|
||||||
// TAGS: build robust payload using original tags and current selection
|
// 5️⃣ Tags
|
||||||
final originalTags = job.tags ?? [];
|
final originalTags = job.tags ?? [];
|
||||||
final currentTags = _selectedTags.toList();
|
final currentTags = _selectedTags.toList();
|
||||||
|
|
||||||
// Only add tags operation if something changed
|
|
||||||
if (_tagsAreDifferent(originalTags, currentTags)) {
|
if (_tagsAreDifferent(originalTags, currentTags)) {
|
||||||
final List<Map<String, dynamic>> finalTagsPayload = [];
|
final List<Map<String, dynamic>> finalTagsPayload = [];
|
||||||
|
|
||||||
// 1) For existing original tags - we need to mark isActive true/false depending on whether they're in currentTags
|
|
||||||
for (var ot in originalTags) {
|
for (var ot in originalTags) {
|
||||||
final isSelected = currentTags.any((ct) =>
|
final isSelected = currentTags.any((ct) =>
|
||||||
(ct.id != null && ct.id == ot.id) ||
|
(ct.id != null && ct.id == ot.id) ||
|
||||||
@ -165,21 +181,25 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Add newly created tags from currentTags that don't have a valid id (id == "0" or null)
|
|
||||||
for (var ct in currentTags.where((c) => c.id == null || c.id == "0")) {
|
for (var ct in currentTags.where((c) => c.id == null || c.id == "0")) {
|
||||||
finalTagsPayload.add({
|
finalTagsPayload.add({"name": ct.name, "isActive": true});
|
||||||
"name": ct.name,
|
|
||||||
"isActive": true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
operations
|
||||||
|
.add({"op": "replace", "path": "/tags", "value": finalTagsPayload});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6️⃣ Job Status
|
||||||
|
final selectedStatus = controller.selectedJobStatus.value;
|
||||||
|
if (selectedStatus != null && selectedStatus.id != job.status?.id) {
|
||||||
operations.add({
|
operations.add({
|
||||||
"op": "replace",
|
"op": "replace",
|
||||||
"path": "/tags",
|
"path": "/statusId", // make sure API expects this field
|
||||||
"value": finalTagsPayload,
|
"value": selectedStatus.id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 7️⃣ Check if anything changed
|
||||||
if (operations.isEmpty) {
|
if (operations.isEmpty) {
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Info",
|
title: "Info",
|
||||||
@ -188,6 +208,7 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 8️⃣ Call API
|
||||||
final success = await ApiService.editServiceProjectJobApi(
|
final success = await ApiService.editServiceProjectJobApi(
|
||||||
jobId: job.id ?? "",
|
jobId: job.id ?? "",
|
||||||
operations: operations,
|
operations: operations,
|
||||||
@ -199,16 +220,13 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
|||||||
message: "Job updated successfully",
|
message: "Job updated successfully",
|
||||||
type: SnackbarType.success);
|
type: SnackbarType.success);
|
||||||
|
|
||||||
// re-fetch job detail and update local selected tags from server response
|
// Re-fetch job detail & update tags locally
|
||||||
await controller.fetchJobDetail(widget.jobId);
|
await controller.fetchJobDetail(widget.jobId);
|
||||||
final updatedJob = controller.jobDetail.value?.data;
|
final updatedJob = controller.jobDetail.value?.data;
|
||||||
|
|
||||||
if (updatedJob != null) {
|
if (updatedJob != null) {
|
||||||
_selectedTags.value = (updatedJob.tags ?? [])
|
_selectedTags.value = (updatedJob.tags ?? [])
|
||||||
.map((t) => Tag(id: t.id, name: t.name))
|
.map((t) => Tag(id: t.id, name: t.name))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
// UI refresh to reflect tags instantly
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -799,6 +817,127 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildJobStatusCard() {
|
||||||
|
final job = controller.jobDetail.value?.data;
|
||||||
|
if (job == null) return const SizedBox();
|
||||||
|
|
||||||
|
// Existing status info
|
||||||
|
final statusName = job.status?.displayName ?? "N/A";
|
||||||
|
Color statusColor;
|
||||||
|
switch (job.status?.level) {
|
||||||
|
case 1:
|
||||||
|
statusColor = Colors.green;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
statusColor = Colors.orange;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
statusColor = Colors.blue;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
statusColor = Colors.red;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
statusColor = Colors.grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
final editing = isEditing.value;
|
||||||
|
|
||||||
|
// Ensure selectedJobStatus initialized
|
||||||
|
if (editing && controller.selectedJobStatus.value == null) {
|
||||||
|
final existingStatusId = job.status?.id;
|
||||||
|
if (existingStatusId != null && controller.jobStatusList.isNotEmpty) {
|
||||||
|
controller.selectedJobStatus.value =
|
||||||
|
controller.jobStatusList.firstWhere(
|
||||||
|
(s) => s.id == existingStatusId,
|
||||||
|
orElse: () => controller.jobStatusList.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _buildSectionCard(
|
||||||
|
title: "Job Status",
|
||||||
|
titleIcon: Icons.flag_outlined,
|
||||||
|
children: [
|
||||||
|
// 1️⃣ Display existing status
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: statusColor.withOpacity(0.2),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(Icons.flag, color: statusColor, size: 24),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
statusName,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: statusColor),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
"Level: ${job.status?.level ?? '-'}",
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey[700]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 2️⃣ PopupMenuButton for new selection
|
||||||
|
if (editing)
|
||||||
|
Obx(() {
|
||||||
|
final selectedStatus = controller.selectedJobStatus.value;
|
||||||
|
final statuses = controller.jobStatusList;
|
||||||
|
|
||||||
|
return PopupMenuButton<JobStatus>(
|
||||||
|
onSelected: (val) => controller.selectedJobStatus.value = val,
|
||||||
|
itemBuilder: (_) => statuses
|
||||||
|
.map(
|
||||||
|
(s) => PopupMenuItem(
|
||||||
|
value: s,
|
||||||
|
child: Text(s.displayName ?? "N/A"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
child: Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
selectedStatus?.displayName ?? "Select Job Status",
|
||||||
|
style:
|
||||||
|
TextStyle(color: Colors.grey.shade700, fontSize: 14),
|
||||||
|
),
|
||||||
|
const Icon(Icons.arrow_drop_down),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final projectName = widget.projectName;
|
final projectName = widget.projectName;
|
||||||
@ -879,6 +1018,7 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
_buildJobStatusCard(),
|
||||||
_buildAttendanceCard(),
|
_buildAttendanceCard(),
|
||||||
_buildSectionCard(
|
_buildSectionCard(
|
||||||
title: "Job Info",
|
title: "Job Info",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user