📱 Mobile Implementation Guide
API Endpoints (2 calls required):
// 1. Main dashboard data
GET /api/dashboard/agent
Returns: AgentDashboardStatsDto (all dashboard data)
// 2. Agent profile (for name display)
GET /api/Agent/profile
Returns: { firstName, lastName, ... }
Client-Side Calculations Required:
Swift (iOS):
// 1. Active Jobs Count
let activeJobs = dashboard.jobStats.scheduled + dashboard.jobStats.inProgress
// 2. Today's Jobs Count
let todayJobsCount = dashboard.todaySchedule.count
// 3. Time Until Next Job
if let nextJob = dashboard.todaySchedule.first {
let timeInterval = nextJob.scheduledDate.timeIntervalSinceNow
let hours = Int(timeInterval / 3600)
let minutes = Int((timeInterval.truncatingRemainder(dividingBy: 3600)) / 60)
if hours > 0 {
timeUntilLabel.text = "in \(hours) hours"
} else {
timeUntilLabel.text = "in \(minutes) minutes"
}
// Show card only if within 4 hours
nextJobCard.isHidden = hours > 4
}
// 4. Format Date as "Today" or "Tomorrow"
func formatJobDate(_ date: Date) -> String {
if Calendar.current.isDateInToday(date) {
return "Today"
} else if Calendar.current.isDateInTomorrow(date) {
return "Tomorrow"
} else {
let formatter = DateFormatter()
formatter.dateFormat = "MMM d"
return formatter.string(from: date)
}
}
// 5. Format Currency
earningsLabel.text = "£\(String(format: "%.2f", dashboard.earningsData.currentMonthEarnings))"
Kotlin (Android):
// 1. Active Jobs Count
val activeJobs = dashboard.jobStats.scheduled + dashboard.jobStats.inProgress
// 2. Today's Jobs Count
val todayJobsCount = dashboard.todaySchedule.size
// 3. Time Until Next Job
dashboard.todaySchedule.firstOrNull()?.let { nextJob ->
val now = LocalDateTime.now()
val scheduledTime = nextJob.scheduledDate.toLocalDateTime()
val duration = Duration.between(now, scheduledTime)
val hours = duration.toHours()
val minutes = duration.toMinutes() % 60
timeUntilText.text = if (hours > 0) {
"in $hours hours"
} else {
"in $minutes minutes"
}
// Show card only if within 4 hours
nextJobCard.visibility = if (hours <= 4) View.VISIBLE else View.GONE
}
// 4. Format Date as "Today" or "Tomorrow"
fun formatJobDate(date: LocalDateTime): String {
val today = LocalDate.now()
return when (date.toLocalDate()) {
today -> "Today"
today.plusDays(1) -> "Tomorrow"
else -> date.format(DateTimeFormatter.ofPattern("MMM d"))
}
}
// 5. Format Currency
earningsText.text = "£%.2f".format(dashboard.earningsData.currentMonthEarnings)
Dart (Flutter):
// 1. Active Jobs Count
final activeJobs = dashboard.jobStats.scheduled + dashboard.jobStats.inProgress;
// 2. Today's Jobs Count
final todayJobsCount = dashboard.todaySchedule.length;
// 3. Time Until Next Job
if (dashboard.todaySchedule.isNotEmpty) {
final nextJob = dashboard.todaySchedule.first;
final now = DateTime.now();
final difference = nextJob.scheduledDate.difference(now);
String timeUntilText;
if (difference.inHours > 0) {
timeUntilText = 'in ${difference.inHours} hours';
} else {
timeUntilText = 'in ${difference.inMinutes} minutes';
}
// Show card only if within 4 hours
showNextJobCard = difference.inHours <= 4;
}
// 4. Format Date as "Today" or "Tomorrow"
String formatJobDate(DateTime date) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final tomorrow = today.add(Duration(days: 1));
final jobDay = DateTime(date.year, date.month, date.day);
if (jobDay == today) {
return 'Today';
} else if (jobDay == tomorrow) {
return 'Tomorrow';
} else {
return DateFormat('MMM d').format(date);
}
}
// 5. Format Currency
final earningsText = '£${dashboard.earningsData.currentMonthEarnings.toStringAsFixed(2)}';
Java (Android):
// 1. Active Jobs Count
int activeJobs = dashboard.getJobStats().getScheduled() +
dashboard.getJobStats().getInProgress();
// 2. Today's Jobs Count
int todayJobsCount = dashboard.getTodaySchedule().size();
// 3. Time Until Next Job
if (!dashboard.getTodaySchedule().isEmpty()) {
ScheduledJobDto nextJob = dashboard.getTodaySchedule().get(0);
Date now = new Date();
long diffMs = nextJob.getScheduledDate().getTime() - now.getTime();
long hours = TimeUnit.MILLISECONDS.toHours(diffMs);
long minutes = TimeUnit.MILLISECONDS.toMinutes(diffMs) % 60;
String timeUntilText;
if (hours > 0) {
timeUntilText = "in " + hours + " hours";
} else {
timeUntilText = "in " + minutes + " minutes";
}
// Show card only if within 4 hours
nextJobCard.setVisibility(hours <= 4 ? View.VISIBLE : View.GONE);
}
// 4. Format Date as "Today" or "Tomorrow"
private String formatJobDate(Date date) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
Calendar today = Calendar.getInstance();
if (isSameDay(cal, today)) {
return "Today";
}
today.add(Calendar.DAY_OF_YEAR, 1);
if (isSameDay(cal, today)) {
return "Tomorrow";
}
SimpleDateFormat sdf = new SimpleDateFormat("MMM d");
return sdf.format(date);
}
// 5. Format Currency
String earningsText = String.format("£%.2f",
dashboard.getEarningsData().getCurrentMonthEarnings());
Performance Chart Implementation:
iOS (Swift) - Using Charts library:
import Charts
func setupChart() {
// Filter data by period
let filteredData = filterByPeriod(dashboard.performance.performanceTrend, period: selectedPeriod)
// Create data entries
var completionEntries: [ChartDataEntry] = []
var ratingEntries: [ChartDataEntry] = []
for (index, item) in filteredData.enumerated() {
// Completion rate (already 0-1, multiply by 100 for display)
completionEntries.append(ChartDataEntry(x: Double(index),
y: item.completionRate * 100))
// Rating (0-5, convert to percentage)
ratingEntries.append(ChartDataEntry(x: Double(index),
y: item.averageRating * 20))
}
// Create datasets
let completionDataSet = LineChartDataSet(entries: completionEntries,
label: "Completion Rate")
completionDataSet.colors = [UIColor.systemBlue]
completionDataSet.drawCirclesEnabled = false
completionDataSet.lineWidth = 2
let ratingDataSet = LineChartDataSet(entries: ratingEntries,
label: "Customer Rating")
ratingDataSet.colors = [UIColor.systemGreen]
ratingDataSet.drawCirclesEnabled = false
ratingDataSet.lineWidth = 2
ratingDataSet.lineDashLengths = [5, 5]
// Combine datasets
let chartData = LineChartData(dataSets: [completionDataSet, ratingDataSet])
lineChartView.data = chartData
// Configure axes
lineChartView.leftAxis.axisMinimum = 80
lineChartView.leftAxis.axisMaximum = 100
lineChartView.xAxis.labelPosition = .bottom
}
Android (MPAndroidChart):
// Kotlin
import com.github.mikephil.charting.charts.LineChart
import com.github.mikephil.charting.data.*
fun setupChart() {
val chart = findViewById(R.id.performanceChart)
// Filter data by period
val filteredData = filterByPeriod(dashboard.performance.performanceTrend, period)
// Create entries
val completionEntries = ArrayList()
val ratingEntries = ArrayList()
filteredData.forEachIndexed { index, item ->
// Completion rate (multiply by 100 for percentage)
completionEntries.add(Entry(index.toFloat(),
(item.completionRate * 100).toFloat()))
// Rating (convert to percentage)
ratingEntries.add(Entry(index.toFloat(),
(item.averageRating * 20).toFloat()))
}
// Create datasets
val completionDataSet = LineDataSet(completionEntries, "Completion Rate")
completionDataSet.color = Color.parseColor("#1A73E8")
completionDataSet.setDrawCircles(false)
completionDataSet.lineWidth = 2f
val ratingDataSet = LineDataSet(ratingEntries, "Customer Rating")
ratingDataSet.color = Color.parseColor("#16A34A")
ratingDataSet.setDrawCircles(false)
ratingDataSet.lineWidth = 2f
ratingDataSet.enableDashedLine(10f, 10f, 0f)
// Combine and set data
val lineData = LineData(completionDataSet, ratingDataSet)
chart.data = lineData
// Configure chart
chart.axisLeft.axisMinimum = 80f
chart.axisLeft.axisMaximum = 100f
chart.xAxis.position = XAxis.XAxisPosition.BOTTOM
chart.invalidate()
}
Flutter (fl_chart):
import 'package:fl_chart/fl_chart.dart';
Widget buildPerformanceChart() {
// Filter data by period
final filteredData = filterByPeriod(
dashboard.performance.performanceTrend,
selectedPeriod
);
// Create spots for lines
final completionSpots = [];
final ratingSpots = [];
for (int i = 0; i < filteredData.length; i++) {
final item = filteredData[i];
// Completion rate (multiply by 100)
completionSpots.add(FlSpot(i.toDouble(), item.completionRate * 100));
// Rating (convert to percentage)
ratingSpots.add(FlSpot(i.toDouble(), item.averageRating * 20));
}
return LineChart(
LineChartData(
minY: 80,
maxY: 100,
lineBarsData: [
// Completion rate line
LineChartBarData(
spots: completionSpots,
color: Color(0xFF1A73E8),
barWidth: 2,
dotData: FlDotData(show: false),
isCurved: true,
),
// Customer rating line
LineChartBarData(
spots: ratingSpots,
color: Color(0xFF16A34A),
barWidth: 2,
dotData: FlDotData(show: false),
dashArray: [5, 5],
isCurved: true,
),
],
titlesData: FlTitlesData(
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) => Text('${value.toInt()}%'),
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
// Show date labels based on period
if (selectedPeriod == '7d') {
final days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
return Text(days[value.toInt() % 7]);
}
return Text('');
},
),
),
),
),
);
}
// Helper function to filter data
List filterByPeriod(
List data,
String period
) {
final now = DateTime.now();
final cutoff = period == '7d'
? now.subtract(Duration(days: 7))
: period == '30d'
? now.subtract(Duration(days: 30))
: now.subtract(Duration(days: 90));
return data.where((item) => item.date.isAfter(cutoff)).toList();
}
Missing API Fields (Backend TODO):
- Customer Phone Number - Not in ScheduledJobDto (needed for call button)
- Pending Reports Count - Not in jobStats (for stats card)
Important Notes:
- ✅ All data comes from
/api/dashboard/agent except agent name
- ✅ Performance data includes last 90 days, filter client-side
- ✅ Completion rate comes as decimal (0.96), multiply by 100 for display
- ✅ Rating comes as 0-5, multiply by 20 for percentage on dual-axis chart
- ⚠️ Handle empty arrays gracefully (no crashes if no data)
- ⚠️ Check for null values in optional fields