feat: update API endpoints and enhance attendance log handling in job detail screen
This commit is contained in:
parent
d05e26bc87
commit
bbadcc4139
@ -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";
|
||||
|
||||
@ -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 {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user