Refactor service project job handling and improve tag management
This commit is contained in:
parent
65fbef3441
commit
341d779499
@ -63,26 +63,34 @@ class AddServiceProjectJobController extends GetxController {
|
||||
return;
|
||||
}
|
||||
|
||||
final assigneeIds = selectedAssignees.map((e) => e.id).toList();
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
final success = await ApiService.createServiceProjectJobApi(
|
||||
final jobId = await ApiService.createServiceProjectJobApi(
|
||||
title: titleCtrl.text.trim(),
|
||||
description: descCtrl.text.trim(),
|
||||
projectId: projectId,
|
||||
branchId: selectedBranch.value?.id,
|
||||
assignees: assigneeIds.map((id) => {"id": id}).toList(),
|
||||
assignees: selectedAssignees // payload mapping
|
||||
.map((e) => {"employeeId": e.id, "isActive": true})
|
||||
.toList(),
|
||||
startDate: startDate.value!,
|
||||
dueDate: dueDate.value!,
|
||||
tags: enteredTags.map((tag) => {"name": tag}).toList(),
|
||||
tags: enteredTags
|
||||
.map((tag) => {"id": null, "name": tag, "isActive": true})
|
||||
.toList(),
|
||||
);
|
||||
|
||||
isLoading.value = false;
|
||||
|
||||
if (success) {
|
||||
if (jobId != null) {
|
||||
if (Get.isRegistered<ServiceProjectDetailsController>()) {
|
||||
Get.find<ServiceProjectDetailsController>().refreshJobsAfterAdd();
|
||||
final detailsCtrl = Get.find<ServiceProjectDetailsController>();
|
||||
|
||||
// 🔥 1. Refresh job LIST
|
||||
detailsCtrl.refreshJobsAfterAdd();
|
||||
|
||||
// 🔥 2. Refresh job DETAILS (FULL DATA - including tags and assignees)
|
||||
await detailsCtrl.fetchJobDetail(jobId);
|
||||
}
|
||||
|
||||
Get.back();
|
||||
|
||||
@ -595,8 +595,7 @@ class ApiService {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Create a new Service Project Job
|
||||
static Future<bool> createServiceProjectJobApi({
|
||||
static Future<String?> createServiceProjectJobApi({
|
||||
required String title,
|
||||
required String description,
|
||||
required String projectId,
|
||||
@ -623,32 +622,22 @@ class ApiService {
|
||||
try {
|
||||
final response = await _postRequest(endpoint, body);
|
||||
|
||||
if (response == null) {
|
||||
logSafe("Create Service Project Job failed: null response",
|
||||
level: LogLevel.error);
|
||||
return false;
|
||||
}
|
||||
|
||||
logSafe(
|
||||
"Create Service Project Job response status: ${response.statusCode}");
|
||||
logSafe("Create Service Project Job response body: ${response.body}");
|
||||
if (response == null) return null;
|
||||
|
||||
final json = jsonDecode(response.body);
|
||||
|
||||
if (json['success'] == true) {
|
||||
logSafe("Service Project Job created successfully: ${json['data']}");
|
||||
return true;
|
||||
} else {
|
||||
logSafe(
|
||||
"Failed to create Service Project Job: ${json['message'] ?? 'Unknown error'}",
|
||||
level: LogLevel.warning,
|
||||
);
|
||||
return false;
|
||||
final jobId = json['data']?['id'];
|
||||
logSafe("Service Project Job created successfully: $jobId");
|
||||
return jobId;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e, stack) {
|
||||
logSafe("Exception during createServiceProjectJobApi: $e",
|
||||
level: LogLevel.error);
|
||||
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -671,8 +660,7 @@ class ApiService {
|
||||
'pageNumber': pageNumber.toString(),
|
||||
'pageSize': pageSize.toString(),
|
||||
'isActive': isActive.toString(),
|
||||
if (isArchive)
|
||||
'isArchive': 'true',
|
||||
if (isArchive) 'isArchive': 'true',
|
||||
};
|
||||
|
||||
final response = await _getRequest(endpoint, queryParams: queryParams);
|
||||
|
||||
@ -54,10 +54,13 @@ class CustomAppBar extends StatelessWidget
|
||||
),
|
||||
title: Padding(
|
||||
padding: MySpacing.only(right: horizontalPadding, left: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
MyText.titleLarge(
|
||||
title,
|
||||
@ -73,7 +76,6 @@ class CustomAppBar extends StatelessWidget
|
||||
projectController.selectedProject?.name ??
|
||||
'Select Project';
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.folder_open,
|
||||
size: 14, color: onPrimaryColor),
|
||||
@ -84,6 +86,7 @@ class CustomAppBar extends StatelessWidget
|
||||
fontWeight: 500,
|
||||
color: onPrimaryColor.withOpacity(0.8),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
MySpacing.width(2),
|
||||
@ -96,6 +99,9 @@ class CustomAppBar extends StatelessWidget
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: MySpacing.only(right: horizontalPadding),
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -324,9 +324,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();
|
||||
@ -356,16 +353,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),
|
||||
|
||||
@ -247,6 +247,43 @@ class _UserProfileBarState extends State<UserProfileBar>
|
||||
|
||||
final tenants = tenantSwitchController.tenants;
|
||||
if (tenants.isEmpty) return _noTenantContainer();
|
||||
// If only one organization, don't show switch option
|
||||
if (tenants.length == 1) {
|
||||
final selectedTenant = tenants.first;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey.shade300, width: 1),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
color: Colors.grey.shade200,
|
||||
child: TenantLogo(logoImage: selectedTenant.logoImage),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
selectedTenant.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: contentTheme.primary),
|
||||
),
|
||||
),
|
||||
const Icon(Icons.check_circle, color: Colors.green, size: 18),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final selectedTenant = TenantService.currentTenant;
|
||||
|
||||
|
||||
@ -47,11 +47,13 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
@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 +63,6 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
job.dueDate ?? '',
|
||||
format: "yyyy-MM-dd");
|
||||
_selectedAssignees.value = job.assignees ?? [];
|
||||
_selectedTags.value = job.tags ?? [];
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -76,7 +77,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 +128,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 +198,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 +221,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 +468,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 +480,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 +508,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 +565,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 +592,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 +624,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 +660,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 +683,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 +709,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 +733,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 +750,7 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
errorBuilder: (_, __, ___) => const Icon(
|
||||
Icons.broken_image,
|
||||
size: 40,
|
||||
color: Colors.grey,
|
||||
),
|
||||
color: Colors.grey),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -730,12 +776,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)),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -915,9 +957,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),
|
||||
@ -937,8 +978,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