📱 Mobile Implementation Guide
API Endpoints Required:
- POST /api/AgentOwnJobs/list - Returns paginated list of agent's jobs
- GET /api/AgentOwnJobs/{id} - Get specific job details
- POST /api/AgentOwnJobs/{id}/accept - Accept a job
- POST /api/AgentOwnJobs/{id}/reject - Reject/decline job with reason
- POST /api/AgentOwnJobs/{id}/schedule - Schedule an accepted job
- POST /api/AgentOwnJobs/{id}/complete - Mark job as completed
- POST /api/AgentOwnJobs/{id}/start-travel - Mark travel started
- POST /api/AgentOwnJobs/{id}/arrive - Mark arrived on site
Client-Side Calculations Required:
Note: All field mappings are shown via tooltips when hovering over UI elements.
This documentation covers the calculations and logic that tooltips cannot explain.
1. Tab Badge Counts:
// Swift (iOS)
// All Active count - exclude completed/cancelled
let allActiveCount = jobs.filter {
!["Completed", "Cancelled", "Rejected"].contains($0.status)
}.count
// Urgent count - based on deadline OR priority
let urgentCount = jobs.filter { job in
if let deadline = job.acceptanceDeadline {
return deadline.timeIntervalSinceNow < 14400 // 4 hours
}
return job.priority == "Urgent" || job.priority == "High"
}.count
// Kotlin (Android)
val allActiveCount = jobs.count {
it.status !in listOf("Completed", "Cancelled", "Rejected")
}
val urgentCount = jobs.count { job ->
job.acceptanceDeadline?.let {
(it.time - System.currentTimeMillis()) < 14400000 // 4 hours in ms
} ?: (job.priority in listOf("Urgent", "High"))
}
// Dart (Flutter)
final allActiveCount = jobs.where((j) =>
!['Completed', 'Cancelled', 'Rejected'].contains(j.status)
).length;
final urgentCount = jobs.where((j) {
if (j.acceptanceDeadline != null) {
return j.acceptanceDeadline.difference(DateTime.now()).inHours < 4;
}
return j.priority == 'Urgent' || j.priority == 'High';
}).length;
2. Time Countdown Calculations:
// Swift - "30 min to decide" from acceptanceDeadline
func timeRemaining(from deadline: Date) -> String {
let minutes = Calendar.current.dateComponents([.minute],
from: Date(), to: deadline).minute ?? 0
if minutes < 0 {
return "Overdue"
} else if minutes < 60 {
return "\(minutes) min to decide"
} else if minutes < 1440 { // Less than 24 hours
return "\(minutes / 60) hrs to decide"
} else {
return "\(minutes / 1440) days to decide"
}
}
// Kotlin - Days until completion deadline
fun daysUntil(date: Date): String {
val days = TimeUnit.DAYS.convert(
date.time - System.currentTimeMillis(),
TimeUnit.MILLISECONDS
)
return when {
days < 0 -> "Overdue"
days == 0L -> "Due today"
days == 1L -> "1 day left"
else -> "$days days left"
}
}
// Dart - Report deadline (24hrs after completion)
String reportDeadline(DateTime completionDate) {
final deadline = completionDate.add(Duration(hours: 24));
final remaining = deadline.difference(DateTime.now());
if (remaining.isNegative) {
return "Report overdue";
} else if (remaining.inHours < 1) {
return "Report due in ${remaining.inMinutes} mins";
} else {
return "Report due in ${remaining.inHours} hrs";
}
}
3. Date/Time Formatting:
// Swift - Format as "Today", "Tomorrow", or date
func formatScheduledDate(_ date: Date, _ time: String?) -> String {
let calendar = Calendar.current
let dateString: String
if calendar.isDateInToday(date) {
dateString = "Today"
} else if calendar.isDateInTomorrow(date) {
dateString = "Tomorrow"
} else {
let formatter = DateFormatter()
formatter.dateFormat = "MMM d"
dateString = formatter.string(from: date)
}
if let time = time {
return "\(dateString), \(time)"
}
return dateString
}
// Kotlin
fun formatScheduledDate(date: Date, time: String?): String {
val calendar = Calendar.getInstance()
val today = Calendar.getInstance()
val dateString = when {
isSameDay(date, today) -> "Today"
isTomorrow(date, today) -> "Tomorrow"
else -> SimpleDateFormat("MMM d", Locale.getDefault()).format(date)
}
return time?.let { "$dateString, $it" } ?: dateString
}
// Dart
String formatScheduledDate(DateTime date, String? time) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final tomorrow = today.add(Duration(days: 1));
String dateString;
if (date.isAfter(today) && date.isBefore(tomorrow)) {
dateString = "Today";
} else if (date.isAfter(tomorrow) &&
date.isBefore(tomorrow.add(Duration(days: 1)))) {
dateString = "Tomorrow";
} else {
dateString = DateFormat('MMM d').format(date);
}
return time != null ? "$dateString, $time" : dateString;
}
4. Elapsed Time Calculation (On Site):
// Swift - Calculate time since arrival
func elapsedTime(since arrivalTime: Date) -> String {
let minutes = Int(Date().timeIntervalSince(arrivalTime) / 60)
if minutes < 60 {
return "\(minutes) mins elapsed"
} else {
let hours = minutes / 60
let mins = minutes % 60
return "\(hours)h \(mins)m elapsed"
}
}
// Kotlin
fun elapsedTime(arrivalTime: Date): String {
val minutes = TimeUnit.MINUTES.convert(
System.currentTimeMillis() - arrivalTime.time,
TimeUnit.MILLISECONDS
).toInt()
return when {
minutes < 60 -> "$minutes mins elapsed"
else -> {
val hours = minutes / 60
val mins = minutes % 60
"${hours}h ${mins}m elapsed"
}
}
}
// Dart
String elapsedTime(DateTime arrivalTime) {
final duration = DateTime.now().difference(arrivalTime);
final minutes = duration.inMinutes;
if (minutes < 60) {
return '$minutes mins elapsed';
} else {
final hours = minutes ~/ 60;
final mins = minutes % 60;
return '${hours}h ${mins}m elapsed';
}
}
5. ETA Display (En Route):
// NOTE: ETA fields are CONDITIONALLY added by API - ONLY for En Route jobs
// Condition: status === "InProgress" AND travelStartTime !== null
// When these conditions are met, API adds:
// - eta: "11:15 AM" (formatted time)
// - etaDateTime: "2024-12-06T11:15:00" (ISO datetime)
// - distanceToSite: "2.3 miles" (formatted driving distance)
// - durationToSite: "15 mins" (formatted driving duration)
// - isEnRoute: true (flag to indicate En Route status)
// These fields DO NOT exist for other jobs - check for existence before use!
// Swift - Display ETA from API
if job.status == "InProgress" && job.travelStartTime != nil {
// API provides these fields for En Route jobs:
etaLabel.text = job.eta ?? "--" // "11:15 AM"
distanceLabel.text = job.distanceToSite ?? "--" // "2.3 miles"
durationLabel.text = job.durationToSite ?? "--" // "15 mins"
// Use etaDateTime for calculations if needed
if let etaDateStr = job.etaDateTime,
let etaDate = ISO8601DateFormatter().date(from: etaDateStr) {
// Can calculate time until arrival, etc.
}
}
// Kotlin - Display ETA from API
if (job.status == "InProgress" && job.travelStartTime != null) {
// API provides these fields for En Route jobs:
etaText.text = job.eta ?: "--" // "11:15 AM"
distanceText.text = job.distanceToSite ?: "--" // "2.3 miles"
durationText.text = job.durationToSite ?: "--" // "15 mins"
// Use etaDateTime for calculations if needed
job.etaDateTime?.let { etaDateStr ->
val etaDate = Instant.parse(etaDateStr)
// Can calculate time until arrival, etc.
}
}
// Dart/Flutter - Display ETA from API
if (job.status == 'InProgress' && job.travelStartTime != null) {
// API provides these fields for En Route jobs:
Text(job.eta ?? '--'), // "11:15 AM"
Text(job.distanceToSite ?? '--'), // "2.3 miles"
Text(job.durationToSite ?? '--'), // "15 mins"
// Use etaDateTime for calculations if needed
if (job.etaDateTime != null) {
final etaDate = DateTime.parse(job.etaDateTime!);
// Can calculate time until arrival, etc.
}
}
API Response Structure:
{
data: [ // Array of SubJobViewDto
{
// Core fields
id: 123,
ourJobReference: "GSC-2024-1847", // Display after client name
subJobType: "MeterReading", // From ClientJob.JobType
status: "Assigned", // Current status
priority: "Normal", // "Low|Normal|High|Urgent"
// Client info
clientName: "British Gas", // From Client via navigation
// Schedule/Completion
scheduledDate: "2024-12-06T00:00:00",
scheduledTime: "14:30",
completionDate: null,
requiredCompletionDate: "2024-12-10T00:00:00",
// Mobile-specific fields
acceptanceDeadline: "2024-12-05T15:30:00", // For countdown timer (status=Assigned)
travelStatus: "NotStarted", // Status: "NotStarted"|"EnRoute"|"OnSite"|"Completed"
travelStartTime: null, // DateTime when agent started travel
arrivalTime: null, // DateTime when agent arrived on site
customerTelephone: "07123456789", // For Call Client button
// Issues
hasIssues: false,
issueDescription: null,
attemptTime: null,
// Reports/Images
hasReport: false, // For filtering
imageCount: 0,
// Distance
distance: 2.3, // Miles from agent base location
// Job location (for ETA calculation)
jobLatitude: 51.5074, // CustomerLatitude from ClientJobGroup
jobLongitude: -0.1278, // CustomerLongitude from ClientJobGroup
// *** CONDITIONAL FIELDS - ONLY for En Route jobs (status="InProgress" with travelStartTime !== null) ***
// eta: "11:15 AM", // (CONDITIONAL - En Route only)
// etaDateTime: "2024-12-06T11:15:00", // (CONDITIONAL - En Route only)
// distanceToSite: "2.3 miles", // (CONDITIONAL - En Route only)
// durationToSite: "15 mins", // (CONDITIONAL - En Route only)
// isEnRoute: true, // (CONDITIONAL - En Route only)
// Other desktop fields also included...
price: 45.00,
isVerified: false,
isInvoiced: false
}
],
totalCount: 47,
pageNumber: 1,
pageSize: 20
}
Job Grouping Logic:
⚡ Action Required Section
// Swift/iOS
let actionRequired = jobs.filter { $0.status == "Assigned" }
// Kotlin/Android
val actionRequired = jobs.filter { it.status == "Assigned" }
// Dart/Flutter
final actionRequired = jobs.where((j) => j.status == 'Assigned').toList();
- Display: subJobType, clientName, ourJobReference, distance, scheduledDate/Time
- Time Countdown: Calculate from acceptanceDeadline - show "30 min to decide"
- Actions: POST /api/AgentOwnJobs/{id}/accept or /reject
- Visual Priority: Red background if acceptanceDeadline < 4 hours
📅 Needs Scheduling Section
// Swift/iOS
let needsScheduling = jobs.filter {
$0.status == "Accepted" && $0.scheduledDate == nil
}
// Kotlin/Android
val needsScheduling = jobs.filter {
it.status == "Accepted" && it.scheduledDate == null
}
// Dart/Flutter
final needsScheduling = jobs.where((j) =>
j.status == 'Accepted' && j.scheduledDate == null
).toList();
- Display: subJobType, clientName, ourJobReference, distance, requiredCompletionDate
- Days Calculation: Show "3 days left" from requiredCompletionDate
- Action: POST /api/AgentOwnJobs/{id}/schedule
- Visual Priority: Amber/Yellow if requiredCompletionDate < 3 days
📝 Completed - Reports Pending Section
// Swift/iOS
let reportsPending = jobs.filter {
$0.status == "Completed" && $0.hasReport == false
}
// Kotlin/Android
val reportsPending = jobs.filter {
it.status == "Completed" && !it.hasReport
}
// Dart/Flutter
final reportsPending = jobs.where((j) =>
j.status == 'Completed' && !j.hasReport
).toList();
- Display: subJobType, clientName, ourJobReference, completionDate
- Report Deadline: completionDate + 24 hours (calculate client-side)
- Action: Navigate to report submission screen
- Visual: Green background to show completed
📍 Upcoming Scheduled Jobs Section
// Swift/iOS
let scheduled = jobs
.filter { $0.status == "Scheduled" }
.sorted { $0.scheduledDate < $1.scheduledDate }
// Kotlin/Android
val scheduled = jobs
.filter { it.status == "Scheduled" }
.sortedBy { it.scheduledDate }
// Dart/Flutter
final scheduled = jobs
.where((j) => j.status == 'Scheduled')
.toList()
..sort((a, b) => a.scheduledDate.compareTo(b.scheduledDate));
- Display: subJobType, clientName, ourJobReference, scheduledDate/Time, distance
- No actions: Just informational display
- Visual: Default gray/neutral background
🚀 Currently Active Jobs Section
// Swift/iOS
let activeJobs = jobs.filter { $0.status == "InProgress" }
// Then check travelStatus for sub-grouping
// Kotlin/Android
val activeJobs = jobs.filter { it.status == "InProgress" }
// Then check travelStatus for sub-grouping
// Dart/Flutter
final activeJobs = jobs.where((j) => j.status == 'InProgress').toList();
// Then check travelStatus for sub-grouping
- En Route (travelStatus == "EnRoute"):
- Show: travelStartTime, eta (from API), distanceToSite, durationToSite
- Actions: Update Location, Call Client (tel:customerTelephone)
- On Site (travelStatus == "OnSite"):
- Show: arrivalTime, elapsed time, imageCount + " photos"
- Actions: Complete Visit, Report Issue
- Visual: Blue background for active status
⚠️ Jobs With Issues Section
// Swift/iOS
let jobsWithIssues = jobs.filter { $0.hasIssues == true }
// Kotlin/Android
val jobsWithIssues = jobs.filter { it.hasIssues }
// Dart/Flutter
final jobsWithIssues = jobs.where((j) => j.hasIssues).toList();
- Display: subJobType, clientName, ourJobReference, issueDescription, attemptTime
- Actions: Reschedule, Contact Office
- Visual: Amber/Yellow background for warning
Priority Color Scheme:
// Color scheme for job cards based on urgency
enum JobPriority {
case urgent // Red - #DC2626 (acceptanceDeadline < 4hrs OR priority == "Urgent")
case warning // Amber - #F59E0B (completionDeadline < 3 days OR priority == "High")
case success // Green - #16A34A (status == "Completed")
case active // Blue - #1A73E8 (status == "InProgress")
case normal // Gray - #D4D4D4 (default for scheduled)
case cancelled // Black - #000000 (status == "Cancelled" with 0.7 opacity)
}
Action Button API Payloads:
Accept: POST /api/AgentOwnJobs/{id}/accept
Body: {} // Empty
Decline: POST /api/AgentOwnJobs/{id}/reject
Body: { "reason": "Unable to reach location in time" }
Schedule: POST /api/AgentOwnJobs/{id}/schedule
Body: { "scheduledDate": "2024-12-06T00:00:00", "scheduledTime": "14:30" }
Start Travel: POST /api/AgentOwnJobs/{id}/start-travel
Body: { "currentLocation": { "lat": 51.5074, "lng": -0.1278 } }
Arrive: POST /api/AgentOwnJobs/{id}/arrive
Body: { "arrivalTime": "2024-12-06T14:25:00" }
Complete: POST /api/AgentOwnJobs/{id}/complete
Body: { "completionNotes": "All tasks completed successfully" }
Report Issue: POST /api/AgentOwnJobs/{id}/report-issue
Body: { "issueType": "NoAccess", "description": "Keysafe code not working" }
Phone Call Integration:
// Swift/iOS
if let phone = job.customerTelephone,
let url = URL(string: "tel://\(phone)") {
UIApplication.shared.open(url)
}
// Kotlin/Android
val intent = Intent(Intent.ACTION_DIAL).apply {
data = Uri.parse("tel:${job.customerTelephone}")
}
startActivity(intent)
// Flutter
import 'package:url_launcher/url_launcher.dart';
final Uri phoneUri = Uri(scheme: 'tel', path: job.customerTelephone);
await launchUrl(phoneUri);
Important Notes:
- Report Deadline: Not in API - calculate as completionDate + 24 hours
- Reference Format: Display as: clientName + " • #" + ourJobReference
- Distance: API returns miles as decimal (2.3 = "2.3 miles")
- Photos: Display imageCount as "X photos taken"
- Timestamps: All dates/times in ISO 8601 format
- Travel Status: Calculated based on timestamps - NotStarted, EnRoute, OnSite, Completed
- ETA Fields: Server-provided for En Route jobs only - eta, etaDateTime, distanceToSite, durationToSite, isEnRoute