done with tag reflection on details screen after edit
This commit is contained in:
parent
260b762724
commit
66c013f797
@ -1,3 +1,4 @@
|
||||
// service_project_details_screen_controller.dart
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/model/service_project/service_projects_details_model.dart';
|
||||
@ -76,9 +77,7 @@ class ServiceProjectDetailsController extends GetxController {
|
||||
final lowerSearch = searchText.toLowerCase();
|
||||
return job.title.toLowerCase().contains(lowerSearch) ||
|
||||
(job.description.toLowerCase().contains(lowerSearch)) ||
|
||||
(job.tags?.any(
|
||||
(tag) => tag.name.toLowerCase().contains(lowerSearch)) ??
|
||||
false);
|
||||
(job.tags?.any((tag) => tag.name.toLowerCase().contains(lowerSearch)) ?? false);
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
@ -93,10 +92,7 @@ class ServiceProjectDetailsController extends GetxController {
|
||||
teamErrorMessage.value = '';
|
||||
|
||||
try {
|
||||
final result = await ApiService.getServiceProjectAllocationList(
|
||||
projectId: projectId.value,
|
||||
isActive: true,
|
||||
);
|
||||
final result = await ApiService.getServiceProjectAllocationList(projectId: projectId.value, isActive: true);
|
||||
|
||||
if (result != null) {
|
||||
teamList.value = result;
|
||||
@ -120,14 +116,12 @@ class ServiceProjectDetailsController extends GetxController {
|
||||
errorMessage.value = '';
|
||||
|
||||
try {
|
||||
final result =
|
||||
await ApiService.getServiceProjectDetailApi(projectId.value);
|
||||
final result = await ApiService.getServiceProjectDetailApi(projectId.value);
|
||||
|
||||
if (result != null && result.data != null) {
|
||||
projectDetail.value = result.data!;
|
||||
} else {
|
||||
errorMessage.value =
|
||||
result?.message ?? "Failed to fetch project details";
|
||||
errorMessage.value = result?.message ?? "Failed to fetch project details";
|
||||
}
|
||||
} catch (e) {
|
||||
errorMessage.value = "Error: $e";
|
||||
@ -146,8 +140,7 @@ class ServiceProjectDetailsController extends GetxController {
|
||||
attendanceMessage.value = '';
|
||||
|
||||
try {
|
||||
final result =
|
||||
await ApiService.getJobAttendanceLog(attendanceId: attendanceId);
|
||||
final result = await ApiService.getJobAttendanceLog(attendanceId: attendanceId);
|
||||
|
||||
if (result != null) {
|
||||
attendanceLog.value = result;
|
||||
@ -210,10 +203,7 @@ class ServiceProjectDetailsController extends GetxController {
|
||||
pageNumber = 1;
|
||||
hasMoreJobs.value = true;
|
||||
|
||||
await Future.wait([
|
||||
fetchProjectDetail(),
|
||||
fetchProjectJobs(),
|
||||
]);
|
||||
await Future.wait([fetchProjectDetail(), fetchProjectJobs()]);
|
||||
}
|
||||
|
||||
// -------------------- Job Detail --------------------
|
||||
@ -258,13 +248,11 @@ class ServiceProjectDetailsController extends GetxController {
|
||||
}
|
||||
|
||||
if (permission == LocationPermission.deniedForever) {
|
||||
attendanceMessage.value =
|
||||
"Location permission permanently denied. Enable it from settings.";
|
||||
attendanceMessage.value = "Location permission permanently denied. Enable it from settings.";
|
||||
return null;
|
||||
}
|
||||
|
||||
return await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high);
|
||||
return await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
|
||||
} catch (e) {
|
||||
attendanceMessage.value = "Failed to get location: $e";
|
||||
return null;
|
||||
@ -295,8 +283,7 @@ class ServiceProjectDetailsController extends GetxController {
|
||||
if (attachment != null) {
|
||||
final bytes = await attachment.readAsBytes();
|
||||
final base64Data = base64Encode(bytes);
|
||||
final mimeType =
|
||||
lookupMimeType(attachment.path) ?? 'application/octet-stream';
|
||||
final mimeType = lookupMimeType(attachment.path) ?? 'application/octet-stream';
|
||||
attachmentPayload = {
|
||||
"documentId": jobId,
|
||||
"fileName": attachment.path.split('/').last,
|
||||
@ -317,13 +304,10 @@ class ServiceProjectDetailsController extends GetxController {
|
||||
"attachment": attachmentPayload,
|
||||
};
|
||||
|
||||
final success = await ApiService.updateServiceProjectJobAttendance(
|
||||
payload: payload,
|
||||
);
|
||||
final success = await ApiService.updateServiceProjectJobAttendance(payload: payload);
|
||||
|
||||
if (success) {
|
||||
attendanceMessage.value =
|
||||
action == 0 ? "Tagged In successfully" : "Tagged Out successfully";
|
||||
attendanceMessage.value = action == 0 ? "Tagged In successfully" : "Tagged Out successfully";
|
||||
await fetchJobDetail(jobId);
|
||||
} else {
|
||||
attendanceMessage.value = "Failed to update attendance";
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
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://devapi.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";
|
||||
|
||||
@ -220,8 +220,9 @@ class _AddServiceProjectJobBottomSheetState
|
||||
.where((s) => s.isNotEmpty);
|
||||
|
||||
for (final p in parts) {
|
||||
if (!controller.enteredTags.contains(p)) {
|
||||
controller.enteredTags.add(p);
|
||||
final clean = p.replaceAll('_', ' ');
|
||||
if (!controller.enteredTags.contains(clean)) {
|
||||
controller.enteredTags.add(clean);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -239,8 +240,9 @@ class _AddServiceProjectJobBottomSheetState
|
||||
.where((s) => s.isNotEmpty);
|
||||
|
||||
for (final p in parts) {
|
||||
if (!controller.enteredTags.contains(p)) {
|
||||
controller.enteredTags.add(p);
|
||||
final clean = p.replaceAll('_', ' ');
|
||||
if (!controller.enteredTags.contains(clean)) {
|
||||
controller.enteredTags.add(clean);
|
||||
}
|
||||
}
|
||||
controller.tagCtrl.clear();
|
||||
@ -256,8 +258,9 @@ class _AddServiceProjectJobBottomSheetState
|
||||
.where((s) => s.isNotEmpty);
|
||||
|
||||
for (final p in parts) {
|
||||
if (!controller.enteredTags.contains(p)) {
|
||||
controller.enteredTags.add(p);
|
||||
final clean = p.replaceAll('_', ' ');
|
||||
if (!controller.enteredTags.contains(clean)) {
|
||||
controller.enteredTags.add(clean);
|
||||
}
|
||||
}
|
||||
controller.tagCtrl.clear();
|
||||
|
||||
@ -367,9 +367,6 @@ class _AdvancePaymentScreenState extends State<AdvancePaymentScreen>
|
||||
? DateFormat('dd MMM yyyy').format(parsedDate)
|
||||
: (dateStr.isNotEmpty ? dateStr : '—');
|
||||
|
||||
final formattedTime =
|
||||
parsedDate != null ? DateFormat('hh:mm a').format(parsedDate) : '';
|
||||
|
||||
final project = item.name ?? '';
|
||||
final desc = item.title ?? '';
|
||||
final amount = (item.amount ?? 0).toDouble();
|
||||
@ -399,16 +396,6 @@ class _AdvancePaymentScreenState extends State<AdvancePaymentScreen>
|
||||
style:
|
||||
TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
||||
),
|
||||
if (formattedTime.isNotEmpty) ...[
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
formattedTime,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade500,
|
||||
fontStyle: FontStyle.italic),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
|
||||
@ -39,19 +39,23 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
final TextEditingController _dueDateController = TextEditingController();
|
||||
final TextEditingController _tagTextController = TextEditingController();
|
||||
|
||||
// local selected lists used while editing
|
||||
final RxList<Assignee> _selectedAssignees = <Assignee>[].obs;
|
||||
final RxList<Tag> _selectedTags = <Tag>[].obs;
|
||||
|
||||
final RxBool isEditing = false.obs;
|
||||
File? imageAttachment;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = Get.put(ServiceProjectDetailsController());
|
||||
controller = Get.find<ServiceProjectDetailsController>();
|
||||
// fetch and seed local selected lists
|
||||
controller.fetchJobDetail(widget.jobId).then((_) {
|
||||
final job = controller.jobDetail.value?.data;
|
||||
if (job != null) {
|
||||
_selectedTags.value = job.tags ?? [];
|
||||
_selectedTags.value =
|
||||
(job.tags ?? []).map((t) => Tag(id: t.id, name: t.name)).toList();
|
||||
_titleController.text = job.title ?? '';
|
||||
_descriptionController.text = job.description ?? '';
|
||||
_startDateController.text = DateTimeUtils.convertUtcToLocal(
|
||||
@ -61,7 +65,6 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
job.dueDate ?? '',
|
||||
format: "yyyy-MM-dd");
|
||||
_selectedAssignees.value = job.assignees ?? [];
|
||||
_selectedTags.value = job.tags ?? [];
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -76,7 +79,18 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _tagsAreDifferent(List<Tag> original, List<Tag> current) {
|
||||
// Compare by id / name sets (simple equality)
|
||||
final origIds = original.map((t) => t.id ?? '').toSet();
|
||||
final currIds = current.map((t) => t.id ?? '').toSet();
|
||||
final origNames = original.map((t) => t.name?.trim() ?? '').toSet();
|
||||
final currNames = current.map((t) => t.name?.trim() ?? '').toSet();
|
||||
|
||||
return !(origIds == currIds && origNames == currNames);
|
||||
}
|
||||
|
||||
Future<void> _editJob() async {
|
||||
_processTagsInput();
|
||||
final job = controller.jobDetail.value?.data;
|
||||
if (job == null) return;
|
||||
|
||||
@ -116,39 +130,56 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
});
|
||||
}
|
||||
|
||||
final originalAssignees = job.assignees;
|
||||
final assigneesPayload = originalAssignees?.map((a) {
|
||||
// Assignees payload (keep same approach)
|
||||
final originalAssignees = job.assignees ?? [];
|
||||
final assigneesPayload = originalAssignees.map((a) {
|
||||
final isSelected = _selectedAssignees.any((s) => s.id == a.id);
|
||||
return {"employeeId": a.id, "isActive": isSelected};
|
||||
}).toList();
|
||||
|
||||
// add newly added assignees
|
||||
for (var s in _selectedAssignees) {
|
||||
if (!(originalAssignees?.any((a) => a.id == s.id) ?? false)) {
|
||||
assigneesPayload?.add({"employeeId": s.id, "isActive": true});
|
||||
if (!(originalAssignees.any((a) => a.id == s.id))) {
|
||||
assigneesPayload.add({"employeeId": s.id, "isActive": true});
|
||||
}
|
||||
}
|
||||
|
||||
operations.add(
|
||||
{"op": "replace", "path": "/assignees", "value": assigneesPayload});
|
||||
|
||||
final originalTags = job.tags;
|
||||
final replaceTagsPayload = originalTags?.map((t) {
|
||||
final isSelected = _selectedTags.any((s) => s.id == t.id);
|
||||
return {"id": t.id, "name": t.name, "isActive": isSelected};
|
||||
}).toList();
|
||||
// TAGS: build robust payload using original tags and current selection
|
||||
final originalTags = job.tags ?? [];
|
||||
final currentTags = _selectedTags.toList();
|
||||
|
||||
final addTagsPayload = _selectedTags
|
||||
.where((t) => t.id == "0")
|
||||
.map((t) => {"name": t.name, "isActive": true})
|
||||
.toList();
|
||||
// Only add tags operation if something changed
|
||||
if (_tagsAreDifferent(originalTags, currentTags)) {
|
||||
final List<Map<String, dynamic>> finalTagsPayload = [];
|
||||
|
||||
if ((replaceTagsPayload?.isNotEmpty ?? false)) {
|
||||
operations
|
||||
.add({"op": "replace", "path": "/tags", "value": replaceTagsPayload});
|
||||
// 1) For existing original tags - we need to mark isActive true/false depending on whether they're in currentTags
|
||||
for (var ot in originalTags) {
|
||||
final isSelected = currentTags.any((ct) =>
|
||||
(ct.id != null && ct.id == ot.id) ||
|
||||
(ct.name?.trim() == ot.name?.trim()));
|
||||
finalTagsPayload.add({
|
||||
"id": ot.id,
|
||||
"name": ot.name,
|
||||
"isActive": isSelected,
|
||||
});
|
||||
}
|
||||
|
||||
if (addTagsPayload.isNotEmpty) {
|
||||
operations.add({"op": "add", "path": "/tags", "value": addTagsPayload});
|
||||
// 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")) {
|
||||
finalTagsPayload.add({
|
||||
"name": ct.name,
|
||||
"isActive": true,
|
||||
});
|
||||
}
|
||||
|
||||
operations.add({
|
||||
"op": "replace",
|
||||
"path": "/tags",
|
||||
"value": finalTagsPayload,
|
||||
});
|
||||
}
|
||||
|
||||
if (operations.isEmpty) {
|
||||
@ -169,10 +200,18 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
title: "Success",
|
||||
message: "Job updated successfully",
|
||||
type: SnackbarType.success);
|
||||
|
||||
// re-fetch job detail and update local selected tags from server response
|
||||
await controller.fetchJobDetail(widget.jobId);
|
||||
final updatedJob = controller.jobDetail.value?.data;
|
||||
|
||||
if (updatedJob != null) {
|
||||
_selectedTags.value = updatedJob.tags ?? [];
|
||||
_selectedTags.value = (updatedJob.tags ?? [])
|
||||
.map((t) => Tag(id: t.id, name: t.name))
|
||||
.toList();
|
||||
|
||||
// UI refresh to reflect tags instantly
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
isEditing.value = false;
|
||||
@ -184,6 +223,29 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
}
|
||||
}
|
||||
|
||||
void _processTagsInput() {
|
||||
final input = _tagTextController.text;
|
||||
|
||||
// Remove comma behaviour → treat whole input as one tag
|
||||
String tag = input.trim();
|
||||
if (tag.isEmpty) {
|
||||
_tagTextController.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert underscore to space
|
||||
tag = tag.replaceAll("_", " ");
|
||||
|
||||
// Avoid duplicate tags (case-insensitive)
|
||||
if (!_selectedTags
|
||||
.any((t) => (t.name ?? "").toLowerCase() == tag.toLowerCase())) {
|
||||
_selectedTags.add(Tag(id: "0", name: tag));
|
||||
}
|
||||
|
||||
// Clear text field
|
||||
_tagTextController.clear();
|
||||
}
|
||||
|
||||
Future<void> _handleTagAction() async {
|
||||
final job = controller.jobDetail.value?.data;
|
||||
if (job == null) return;
|
||||
@ -408,10 +470,8 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
border: Border.all(color: Colors.grey.shade400),
|
||||
),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
"Tap to select assignees",
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
|
||||
),
|
||||
child: Text("Tap to select assignees",
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[700])),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -422,19 +482,24 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
Widget _tagEditor() {
|
||||
return Obx(() {
|
||||
final editing = isEditing.value;
|
||||
final tags = _selectedTags;
|
||||
final job = controller.jobDetail.value?.data;
|
||||
|
||||
final displayTags = editing ? _selectedTags : (job?.tags ?? []);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
children: tags
|
||||
children: displayTags
|
||||
.map(
|
||||
(t) => Chip(
|
||||
label: Text(t.name ?? ''),
|
||||
onDeleted: editing
|
||||
? () {
|
||||
_selectedTags.remove(t);
|
||||
_selectedTags.removeWhere((x) =>
|
||||
(x.id != null && x.id == t.id) ||
|
||||
(x.name == t.name));
|
||||
}
|
||||
: null,
|
||||
),
|
||||
@ -445,17 +510,21 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
if (editing)
|
||||
TextField(
|
||||
controller: _tagTextController,
|
||||
onSubmitted: (v) {
|
||||
final value = v.trim();
|
||||
if (value.isNotEmpty && !tags.any((t) => t.name == value)) {
|
||||
_selectedTags.add(Tag(id: "0", name: value));
|
||||
onChanged: (value) {
|
||||
// If space or comma typed → process tags immediately
|
||||
if (value.endsWith(" ") || value.contains(",")) {
|
||||
_processTagsInput();
|
||||
}
|
||||
_tagTextController.clear();
|
||||
},
|
||||
onSubmitted: (_) {
|
||||
// Still supports ENTER
|
||||
_processTagsInput();
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: "Type and press enter to add tags",
|
||||
border:
|
||||
OutlineInputBorder(borderRadius: BorderRadius.circular(5)),
|
||||
hintText: "Type tags (space or comma to add multiple tags)",
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
isDense: true,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 12),
|
||||
@ -498,8 +567,7 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
isAttendanceExpanded.value
|
||||
? Icons.expand_less
|
||||
: Icons.expand_more,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
color: Colors.grey[600]),
|
||||
onPressed: () async {
|
||||
isAttendanceExpanded.value =
|
||||
!isAttendanceExpanded.value;
|
||||
@ -526,22 +594,17 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
height: 16,
|
||||
width: 16,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
color: Colors.white, strokeWidth: 2))
|
||||
: Icon(action == 0 ? Icons.login : Icons.logout),
|
||||
label: MyText.bodyMedium(
|
||||
action == 0 ? "Tag In" : "Tag Out",
|
||||
fontWeight: 600,
|
||||
color: Colors.white,
|
||||
),
|
||||
color: Colors.white),
|
||||
onPressed: isLoading ? null : _handleTagAction,
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(5)),
|
||||
backgroundColor:
|
||||
action == 0 ? Colors.green : Colors.red,
|
||||
),
|
||||
@ -563,10 +626,8 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
if (logs.isEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: MyText.bodyMedium(
|
||||
"No attendance logs available",
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
child: MyText.bodyMedium("No attendance logs available",
|
||||
color: Colors.grey[600]),
|
||||
);
|
||||
}
|
||||
|
||||
@ -601,25 +662,21 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
log.action == 0 ? Icons.login : Icons.logout,
|
||||
log.action == 0
|
||||
? Icons.login
|
||||
: Icons.logout,
|
||||
color: log.action == 0
|
||||
? Colors.green
|
||||
: Colors.red,
|
||||
size: 18,
|
||||
),
|
||||
size: 18),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
employeeName,
|
||||
child: Text(employeeName,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"$date | $time",
|
||||
fontWeight: FontWeight.w600))),
|
||||
Text("$date | $time",
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: Colors.grey[700]),
|
||||
),
|
||||
fontSize: 12, color: Colors.grey[700])),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
@ -628,11 +685,8 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
if (log.comment?.isNotEmpty == true)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
log.comment!,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
child: Text(log.comment!,
|
||||
style: const TextStyle(fontSize: 13))),
|
||||
|
||||
// Location
|
||||
if (log.latitude != null && log.longitude != null)
|
||||
@ -657,14 +711,12 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
Icon(Icons.location_on,
|
||||
size: 14, color: Colors.blue),
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
"View Location",
|
||||
Text("View Location",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.blue,
|
||||
decoration:
|
||||
TextDecoration.underline),
|
||||
),
|
||||
TextDecoration.underline)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -683,12 +735,9 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
fit: BoxFit.cover,
|
||||
height: 250,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
const Icon(
|
||||
Icons.broken_image,
|
||||
const Icon(Icons.broken_image,
|
||||
size: 50,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
color: Colors.grey)),
|
||||
),
|
||||
),
|
||||
child: ClipRRect(
|
||||
@ -703,8 +752,7 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
errorBuilder: (_, __, ___) => const Icon(
|
||||
Icons.broken_image,
|
||||
size: 40,
|
||||
color: Colors.grey,
|
||||
),
|
||||
color: Colors.grey),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -730,12 +778,8 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: MyText.bodySmall(label,
|
||||
fontWeight: 600, color: Colors.grey.shade700),
|
||||
),
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: MyText.bodyMedium(value, fontWeight: 500),
|
||||
),
|
||||
fontWeight: 600, color: Colors.grey.shade700)),
|
||||
Expanded(flex: 5, child: MyText.bodyMedium(value, fontWeight: 500)),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -765,17 +809,13 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
appBar: CustomAppBar(
|
||||
title: "Job Details Screen",
|
||||
onBackPressed: () => Get.back(),
|
||||
projectName: projectName,
|
||||
),
|
||||
projectName: projectName),
|
||||
floatingActionButton: Obx(() => FloatingActionButton.extended(
|
||||
onPressed:
|
||||
isEditing.value ? _editJob : () => isEditing.value = true,
|
||||
backgroundColor: contentTheme.primary,
|
||||
label: MyText.bodyMedium(
|
||||
isEditing.value ? "Save" : "Edit",
|
||||
color: Colors.white,
|
||||
fontWeight: 600,
|
||||
),
|
||||
label: MyText.bodyMedium(isEditing.value ? "Save" : "Edit",
|
||||
color: Colors.white, fontWeight: 600),
|
||||
icon: Icon(isEditing.value ? Icons.save : Icons.edit),
|
||||
)),
|
||||
body: Obx(() {
|
||||
@ -806,14 +846,12 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
_editableRow("Title", _titleController),
|
||||
_editableRow("Description", _descriptionController),
|
||||
_dateRangePicker(),
|
||||
],
|
||||
),
|
||||
]),
|
||||
MySpacing.height(12),
|
||||
_buildSectionCard(
|
||||
title: "Project Branch",
|
||||
titleIcon: Icons.account_tree_outlined,
|
||||
children: [_branchDisplay()],
|
||||
),
|
||||
children: [_branchDisplay()]),
|
||||
MySpacing.height(16),
|
||||
_buildSectionCard(
|
||||
title: "Assignees",
|
||||
@ -874,9 +912,8 @@ class JobTimeline extends StatelessWidget {
|
||||
width: 16,
|
||||
height: 16,
|
||||
indicator: DecoratedBox(
|
||||
decoration:
|
||||
BoxDecoration(color: Colors.blue, shape: BoxShape.circle)),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue, shape: BoxShape.circle))),
|
||||
beforeLineStyle: LineStyle(color: Colors.grey.shade300, thickness: 2),
|
||||
endChild: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
@ -896,8 +933,7 @@ class JobTimeline extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(4)),
|
||||
child: MyText.bodySmall(initials, fontWeight: 600),
|
||||
),
|
||||
child: MyText.bodySmall(initials, fontWeight: 600)),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(child: MyText.bodySmall(updatedBy)),
|
||||
]),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user