feat: update API endpoints and enhance attendance log handling in job detail screen

This commit is contained in:
Vaibhav Surve 2025-11-19 15:44:55 +05:30
parent d05e26bc87
commit bbadcc4139
2 changed files with 194 additions and 158 deletions

View File

@ -1,8 +1,10 @@
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 getMasterCurrencies = "/Master/currencies/list";
static const String getMasterExpensesCategories =
@ -145,7 +147,7 @@ class ApiEndpoints {
static const String editServiceProjectJob = "/serviceproject/job/edit";
static const String createServiceProjectJob = "/serviceproject/job/create";
static const String serviceProjectUpateJobAttendance = "/serviceproject/job/attendance";
static const String serviceProjectUpateJobAttendanceLog = "/job/attendance/log";
static const String serviceProjectUpateJobAttendanceLog = "/serviceproject/job/attendance/log";
static const String getServiceProjectUpateJobAllocationList = "/serviceproject/get/allocation/list";
static const String manageServiceProjectUpateJobAllocation = "/serviceproject/manage/allocation";
static const String getTeamRoles = "/master/team-roles/list";

View File

@ -28,6 +28,8 @@ class JobDetailsScreen extends StatefulWidget {
class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
late final ServiceProjectDetailsController controller;
final RxBool isAttendanceExpanded = false.obs;
RxBool isAttendanceLogLoading = false.obs;
final TextEditingController _titleController = TextEditingController();
final TextEditingController _descriptionController = TextEditingController();
@ -152,7 +154,9 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
}
final success = await ApiService.editServiceProjectJobApi(
jobId: job.id, operations: operations);
jobId: job.id,
operations: operations,
);
if (success) {
Get.snackbar("Success", "Job updated successfully");
@ -167,16 +171,15 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
final job = controller.jobDetail.value?.data;
if (job == null) return;
// Determine action based on current/next tagging state
final action = job.nextTaggingAction;
File? attachmentFile;
// Step 1: Ask for comment first (optional)
// Step 1: Show comment bottom sheet
final comment = await showCommentBottomSheet(
context, action == 0 ? "Tag In" : "Tag Out");
if (comment == null) return; // User cancelled
// Step 2: Ask for image optionally using your custom ConfirmDialog
// Step 2: Ask for optional image
await showDialog(
context: context,
builder: (_) => ConfirmDialog(
@ -196,13 +199,23 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
),
);
// Step 3: Perform attendance using controller
// Step 3: Call attendance update
await controller.updateJobAttendance(
jobId: job.id,
action: action == 0 ? 0 : 1,
comment: comment ?? "",
comment: comment,
attachment: attachmentFile,
);
// Step 4: Check message to detect failure
if (controller.attendanceMessage.value.toLowerCase().contains("failed") ||
controller.attendanceMessage.value.toLowerCase().contains("error")) {
Get.snackbar("Error", controller.attendanceMessage.value);
return; // Do NOT close bottom sheet
}
// Success
Get.snackbar("Success", controller.attendanceMessage.value);
}
Widget _buildSectionCard({
@ -444,10 +457,11 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
return Obx(() {
final job = controller.jobDetail.value?.data;
final isLoading = controller.isTagging.value;
final action = job?.nextTaggingAction ?? 0;
final RxBool isExpanded = false.obs;
final action = job?.nextTaggingAction;
final logs = controller.attendanceLog.value?.data ?? [];
if (job == null) return const SizedBox();
return Card(
elevation: 3,
shadowColor: Colors.black12,
@ -468,25 +482,26 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
const Spacer(),
Obx(() => IconButton(
icon: Icon(
isExpanded.value
isAttendanceExpanded.value
? Icons.expand_less
: Icons.expand_more,
color: Colors.grey[600],
),
onPressed: () async {
isExpanded.value = !isExpanded.value;
// Fetch attendance logs only when expanded
if (isExpanded.value && job != null) {
await controller.fetchJobAttendanceLog(job.attendanceId ?? '');
isAttendanceExpanded.value = !isAttendanceExpanded.value;
if (isAttendanceExpanded.value && job != null) {
await controller.fetchJobAttendanceLog(
job.attendanceId ?? '');
}
},
)),
))
],
),
const SizedBox(height: 8),
const Divider(),
// Tag In/Tag Out Button
if (action != null)
Align(
alignment: Alignment.center,
child: SizedBox(
@ -507,8 +522,7 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
fontWeight: 600,
color: Colors.white,
),
onPressed:
isLoading || job == null ? null : _handleTagAction,
onPressed: isLoading ? null : _handleTagAction,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 20),
shape: RoundedRectangleBorder(
@ -520,9 +534,16 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
),
),
// Attendance Logs List
// Attendance Logs
Obx(() {
if (!isExpanded.value) return Container();
if (!isAttendanceExpanded.value) return Container();
if (isAttendanceLogLoading.value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Center(child: CircularProgressIndicator()),
);
}
if (logs.isEmpty) {
return Padding(
@ -539,67 +560,71 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.only(top: 12),
itemCount: logs.length,
separatorBuilder: (_, __) => const SizedBox(height: 12),
separatorBuilder: (_, __) => const SizedBox(height: 8),
itemBuilder: (_, index) {
final log = logs[index];
final employeeName =
"${log.employee.firstName} ${log.employee.lastName}";
final date =
DateTimeUtils.convertUtcToLocal(log.markedAt.toIso8601String(),
format: 'd MMM yyyy');
final time =
DateTimeUtils.convertUtcToLocal(log.markedAt.toIso8601String(),
format: 'hh:mm a');
return Container(
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
padding: const EdgeInsets.all(12),
return Card(
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8)),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header: Icon + Employee + Date + Time
// Top Row: Icon, Employee, Date, Time
Row(
children: [
Icon(
log.action == 0 ? Icons.login : Icons.logout,
color:
log.action == 0 ? Colors.green : Colors.red,
color: log.action == 0
? Colors.green
: Colors.red,
size: 18,
),
const SizedBox(width: 8),
const SizedBox(width: 6),
Expanded(
child: MyText.bodyMedium(
"$employeeName | ${DateTimeUtils.convertUtcToLocal(log.markedAt.toIso8601String(), format: 'd MMM yyyy')}",
fontWeight: 600,
child: Text(
employeeName,
style: const TextStyle(
fontWeight: FontWeight.w600),
),
),
MyText.bodySmall(
"Time: ${DateTimeUtils.convertUtcToLocal(log.markedAt.toIso8601String(), format: 'hh:mm a')}",
color: Colors.grey[700],
Text(
"$date | $time",
style: TextStyle(
fontSize: 12, color: Colors.grey[700]),
),
],
),
const SizedBox(height: 8),
const Divider(height: 1, color: Colors.grey),
const SizedBox(height: 8),
const SizedBox(height: 4),
// Comment / Description
MyText.bodySmall(
"Description: ${log.comment?.isNotEmpty == true ? log.comment : 'No description provided'}",
// Comment
if (log.comment?.isNotEmpty == true)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
log.comment!,
style: const TextStyle(fontSize: 13),
),
),
const SizedBox(height: 8),
// Location
if (log.latitude != null && log.longitude != null)
GestureDetector(
onTap: () async {
final lat =
double.tryParse(log.latitude!) ?? 0.0;
final lon =
double.tryParse(log.longitude!) ?? 0.0;
final lat = double.tryParse(log.latitude!) ?? 0.0;
final lon = double.tryParse(log.longitude!) ?? 0.0;
final url =
'https://www.google.com/maps/search/?api=1&query=$lat,$lon';
if (await canLaunchUrl(Uri.parse(url))) {
@ -607,31 +632,38 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
mode: LaunchMode.externalApplication);
}
},
child: Padding(
padding: const EdgeInsets.only(top: 4),
child: Row(
children: [
mainAxisSize: MainAxisSize.min,
children: const [
Icon(Icons.location_on,
size: 16, color: Colors.blue),
const SizedBox(width: 4),
MyText.bodySmall(
size: 14, color: Colors.blue),
SizedBox(width: 4),
Text(
"View Location",
style: TextStyle(
fontSize: 12,
color: Colors.blue,
decoration: TextDecoration.underline,
decoration: TextDecoration.underline),
),
],
),
),
const SizedBox(height: 8),
),
// Attached Image
if (log.document != null)
GestureDetector(
Padding(
padding: const EdgeInsets.only(top: 4),
child: GestureDetector(
onTap: () => showDialog(
context: context,
builder: (_) => Dialog(
child: Image.network(
log.document!.preSignedUrl,
fit: BoxFit.cover,
height: 400,
height: 250,
errorBuilder: (_, __, ___) => const Icon(
Icons.broken_image,
size: 50,
@ -641,13 +673,13 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
borderRadius: BorderRadius.circular(6),
child: Image.network(
log.document!.thumbPreSignedUrl.isNotEmpty
? log.document!.thumbPreSignedUrl
: log.document!.preSignedUrl,
height: 60,
width: 60,
height: 50,
width: 50,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Icon(
Icons.broken_image,
@ -657,8 +689,10 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
),
),
),
),
],
),
),
);
},
);
@ -668,7 +702,7 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
),
);
});
}
}
@override
Widget build(BuildContext context) {