🔧 DEV MODE - Hover over orange-bordered elements to see field mappings from SubJob/ClientJob DTOs
🔧 Developer API Reference
Primary API Endpoints:
// Get job details
GET /api/subjob/{subJobId}
Authorization: Bearer {token}
// Accept job
POST /api/subjob/{subJobId}/accept
Authorization: Bearer {token}
Body: {
"agentId": "{agentId}",
"acceptedAt": "2024-01-15T14:30:00Z"
}
// Decline job
POST /api/subjob/{subJobId}/decline
Authorization: Bearer {token}
Body: {
"agentId": "{agentId}",
"declineReason": "Too far away",
"declinedAt": "2024-01-15T14:30:00Z"
}
// Get agent performance impact
GET /api/agent/{agentId}/performance/impact?subJobId={subJobId}
Authorization: Bearer {token}
Data Structure (SubJobDetailDto):
{
"subJob": {
"id": 1234,
"ourJobReference": "GSC-2024-1862",
"status": "Assigned",
"priority": "Urgent",
"price": 85.00,
"rateType": "Standard",
"autoDeclineMinutes": 30,
"assignedAt": "2024-01-15T14:00:00Z",
"acceptanceDeadline": "2024-01-15T14:30:00Z",
"scheduledDate": "2024-01-15",
"scheduledTime": "16:00",
"estimatedDurationMinutes": 45,
"notes": "Customer will be home",
"previousVisitNotes": "Gate code: 1234",
"ppeRequirements": "Standard PPE",
"requiresSpecialEquipment": false,
"hasPrerequisites": false,
"assignedAgentName": "John Smith"
},
"clientJob": {
"id": 5678,
"clientName": "British Gas",
"clientPriority": "VIP",
"jobType": "Gas Safety Check",
"jobServiceCategory": "Property",
"clientJobReference": "BG-2024-45678",
"customerName": "Mr. David Johnson",
"customerEmail": "david.johnson@email.com",
"customerTelephone": "0121 456 7890",
"customerMobile": "07700 123456",
"customerAddress": "42 Manchester Road",
"customerCity": "Birmingham",
"customerCounty": "West Midlands",
"customerPostcode": "B15 2JK",
"customerCountry": "United Kingdom",
"customerReference": "CUST-12345",
"groupReference": "GRP-2024-001",
"companyName": "Johnson Properties Ltd",
"preferredContactMethod": "Mobile",
"preferredContactTime": "Morning",
"alternativeContacts": "Wife: 07700 654321",
"clientHelpdeskPhone": "0800 123 4567",
"requiredCompletionDate": "2024-01-15T18:00:00Z",
"requiresUrgentAttention": true,
"visitReason": "Annual gas safety inspection",
"additionalRequirements": "Check all gas appliances",
"parkingType": "Street parking available",
"hasPetsOnSite": true,
"petDetails": "Friendly dog - will be secured",
"otherJobTypeDescription": null,
"propertyDetails": {
"propertyType": "Semi-detached",
"propertySize": "3 bedroom",
"propertyCondition": "Good",
"numberOfRooms": 7,
"numberOfFloors": 2,
"isMultiStory": true,
"occupancyStatus": "Occupied",
"tenantContactName": "Sarah Johnson",
"tenantContactNumber": "07700 111222",
"tenantAvailability": "Available all day",
"keyHolderName": "Neighbor - Mrs Smith",
"keyHolderContact": "07700 333444",
"keySafeLocation": "Side of property",
"keySafeCode": "1234",
"hasAlarmSystem": true,
"alarmCode": "5678",
"accessInstructions": "Ring doorbell twice",
"hasHealthAndSafetyRisks": true,
"healthAndSafetyNotes": "Steep stairs",
"hasAsbestos": false,
"asbestosDetails": null,
"isPestInfested": false,
"pestDetails": null,
"hasWaterSupply": true,
"hasElectricity": true,
"hasGas": true,
"electricityMeterLocation": "Under stairs cupboard",
"gasMeterLocation": "External box - front",
"waterMeterLocation": "Front garden",
"internalStopTapPresent": true,
"internalStopTapLocation": "Kitchen sink",
"externalStopTapPresent": true,
"externalStopTapLocation": "Front path",
"hasPreviouslyBeenServiced": true,
"previousWorkDetails": "Last serviced 12 months ago"
},
"propertyServiceDetails": {
"requiresCertificate": true,
"lastCheckDate": "2023-01-15",
"appliancesToCheck": "Boiler, cooker, fire"
},
"meters": [
{
"product": "Gas",
"serial": "G123456789",
"mpan": "1234567890123",
"type": "Credit",
"size": "Standard",
"location": "External meter box",
"lastReading": "12345",
"lastReadingDate": "2023-12-15",
"isAvailable": true,
"unavailableReason": null
},
{
"product": "Electric",
"serial": "E987654321",
"mpan": "9876543210987",
"type": "Smart",
"size": "Standard",
"location": "Under stairs",
"lastReading": "54321",
"lastReadingDate": "2023-12-15",
"isAvailable": true,
"unavailableReason": null
}
],
"groupImages": [
{
"id": 101,
"description": "Property front",
"uploadedAt": "2024-01-10T10:00:00Z",
"isPdf": false,
"isMandatory": true,
"fileData": "base64..."
}
],
"jobImages": [
{
"id": 201,
"description": "Access instructions",
"uploadedAt": "2024-01-14T15:00:00Z",
"isPdf": true,
"isMandatory": false,
"fileData": "base64..."
}
]
},
"agent": {
"performanceMetrics": {
"acceptanceRate": 0.89,
"completionRate": 0.96,
"averageRating": 4.7,
"bonusAtRisk": 500
}
}
}
Client-Side Calculations Required:
Swift (iOS):
// 1. Auto-decline countdown
let assignedTime = subJob.assignedAt ?? Date()
let autoDeclineSeconds = (subJob.autoDeclineMinutes ?? 30) * 60
let elapsedTime = Date().timeIntervalSince(assignedTime)
let remainingSeconds = max(0, autoDeclineSeconds - elapsedTime)
let minutes = Int(remainingSeconds) / 60
let seconds = Int(remainingSeconds) % 60
let countdownText = String(format: "%02d:%02d", minutes, seconds)
// 2. Distance and travel time (already in SubJob from backend)
let distance = subJob.distance ?? 0
let travelMinutes = subJob.estimatedTravelMinutes ?? 0
let distanceText = String(format: "%.1f miles", distance)
let travelText = "\(travelMinutes) min"
// 3. Performance impact (already calculated by backend)
let currentRate = subJob.currentAcceptanceRate ?? 0
let ifAccepted = subJob.predictedAcceptanceRateIfAccepted ?? 0
let ifDeclined = subJob.predictedAcceptanceRateIfDeclined ?? 0
let acceptanceRateText = String(format: "%.0f%%", currentRate * 100)
let bonusAtRisk = subJob.bonusAtRisk ?? 0
// 4. SLA deadline
if let slaDate = clientJob.requiredCompletionDate {
let hoursRemaining = slaDate.timeIntervalSinceNow / 3600
let slaText = hoursRemaining < 24 ?
String(format: "%.0f hours", hoursRemaining) :
String(format: "%.0f days", hoursRemaining / 24)
}
// 5. Priority bar width
let priorityWidth: CGFloat = switch subJob.priority {
case "Urgent": 1.0
case "High": 0.75
case "Normal": 0.50
case "Low": 0.25
default: 0.5
}
Kotlin (Android):
// 1. Auto-decline countdown
val assignedTime = subJob.assignedAt ?: LocalDateTime.now()
val autoDeclineMinutes = subJob.autoDeclineMinutes ?: 30.0
val elapsed = Duration.between(assignedTime, LocalDateTime.now()).toMinutes()
val remainingMinutes = max(0, autoDeclineMinutes - elapsed)
val minutes = remainingMinutes.toInt()
val seconds = ((remainingMinutes - minutes) * 60).toInt()
val countdownText = String.format("%02d:%02d", minutes, seconds)
// 2. Distance and travel time (from backend)
val distance = subJob.distance ?: 0.0
val travelMinutes = subJob.estimatedTravelMinutes ?: 0
val distanceText = String.format("%.1f miles", distance)
val travelText = "$travelMinutes min"
// 3. Performance impact (from backend)
val currentRate = subJob.currentAcceptanceRate ?: 0.0
val ifAccepted = subJob.predictedAcceptanceRateIfAccepted ?: 0.0
val ifDeclined = subJob.predictedAcceptanceRateIfDeclined ?: 0.0
val acceptanceRateText = String.format("%.0f%%", currentRate * 100)
val bonusAtRisk = subJob.bonusAtRisk ?: 0.0
// 4. SLA deadline
clientJob.requiredCompletionDate?.let { slaDate ->
val hoursRemaining = Duration.between(LocalDateTime.now(), slaDate).toHours()
val slaText = if (hoursRemaining < 24) {
String.format("%.0f hours", hoursRemaining)
} else {
String.format("%.0f days", hoursRemaining / 24)
}
}
// 5. Priority bar width (as fraction)
val priorityWidth = when(subJob.priority) {
"Urgent" -> 1.0f
"High" -> 0.75f
"Normal" -> 0.5f
"Low" -> 0.25f
else -> 0.5f
}
Dart (Flutter):
// 1. Auto-decline countdown
final assignedTime = subJob.assignedAt ?? DateTime.now();
final autoDeclineMinutes = subJob.autoDeclineMinutes ?? 30;
final elapsed = DateTime.now().difference(assignedTime).inSeconds;
final remainingSeconds = max(0, (autoDeclineMinutes * 60) - elapsed);
final minutes = remainingSeconds ~/ 60;
final seconds = remainingSeconds % 60;
final countdownText = '${minutes.toString().padLeft(2, '0')}:'
'${seconds.toString().padLeft(2, '0')}';
// 2. Distance and travel time (from backend)
final distance = subJob.distance ?? 0;
final travelMinutes = subJob.estimatedTravelMinutes ?? 0;
final distanceText = '${distance.toStringAsFixed(1)} miles';
final travelText = '$travelMinutes min';
// 3. Performance impact (from backend)
final currentRate = subJob.currentAcceptanceRate ?? 0;
final ifAccepted = subJob.predictedAcceptanceRateIfAccepted ?? 0;
final ifDeclined = subJob.predictedAcceptanceRateIfDeclined ?? 0;
final acceptanceRateText = '${(currentRate * 100).toStringAsFixed(0)}%';
final bonusAtRisk = subJob.bonusAtRisk ?? 0;
// 4. SLA deadline
if (clientJob.requiredCompletionDate != null) {
final hoursRemaining = clientJob.requiredCompletionDate!
.difference(DateTime.now()).inHours;
final slaText = hoursRemaining < 24
? '$hoursRemaining hours'
: '${(hoursRemaining / 24).round()} days';
}
// 5. Priority bar width
final priorityWidth = switch(subJob.priority) {
'Urgent' => 1.0,
'High' => 0.75,
'Normal' => 0.5,
'Low' => 0.25,
_ => 0.5
};
Java (Android):
// 1. Auto-decline countdown
Date assignedTime = subJob.getAssignedAt() != null ?
subJob.getAssignedAt() : new Date();
double autoDeclineMinutes = subJob.getAutoDeclineMinutes() != null ?
subJob.getAutoDeclineMinutes() : 30.0;
long elapsedMs = System.currentTimeMillis() - assignedTime.getTime();
long remainingMs = Math.max(0, (long)(autoDeclineMinutes * 60000) - elapsedMs);
int minutes = (int)(remainingMs / 60000);
int seconds = (int)((remainingMs % 60000) / 1000);
String countdownText = String.format("%02d:%02d", minutes, seconds);
// 2. Distance and travel time (from backend)
double distance = subJob.getDistance() != null ?
subJob.getDistance() : 0.0;
int travelMinutes = subJob.getEstimatedTravelMinutes() != null ?
subJob.getEstimatedTravelMinutes() : 0;
String distanceText = String.format("%.1f miles", distance);
String travelText = travelMinutes + " min";
// 3. Performance impact (from backend)
double currentRate = subJob.getCurrentAcceptanceRate() != null ?
subJob.getCurrentAcceptanceRate() : 0.0;
double ifAccepted = subJob.getPredictedAcceptanceRateIfAccepted() != null ?
subJob.getPredictedAcceptanceRateIfAccepted() : 0.0;
double ifDeclined = subJob.getPredictedAcceptanceRateIfDeclined() != null ?
subJob.getPredictedAcceptanceRateIfDeclined() : 0.0;
String acceptanceRateText = String.format("%.0f%%", currentRate * 100);
BigDecimal bonusAtRisk = subJob.getBonusAtRisk() != null ?
subJob.getBonusAtRisk() : BigDecimal.ZERO;
// 4. SLA deadline
if (clientJob.getRequiredCompletionDate() != null) {
long hoursRemaining = TimeUnit.MILLISECONDS.toHours(
clientJob.getRequiredCompletionDate().getTime() -
System.currentTimeMillis()
);
String slaText = hoursRemaining < 24 ?
String.format("%d hours", hoursRemaining) :
String.format("%d days", hoursRemaining / 24);
}
// 5. Priority bar width (for ProgressBar or ConstraintLayout)
float priorityWidth;
switch(subJob.getPriority()) {
case "Urgent": priorityWidth = 1.0f; break;
case "High": priorityWidth = 0.75f; break;
case "Normal": priorityWidth = 0.5f; break;
case "Low": priorityWidth = 0.25f; break;
default: priorityWidth = 0.5f;
}
Conditional Field Display Logic (Based on 103 Visibility Conditions):
SECTION 5: SPECIFIC TASKS - Main Category Conditions:
1. Property Tasks Section:
Condition: jobServiceCategory === 'Property'
Shows: propertyDetails.* and propertyServiceDetails.*
Job-Type Specific Fields Within Property:
• PlumbingServices → plumbing issue, leaking, blockage fields
• ElectricalInstallations → electrical issue, outage, fuse box fields
• Building Repairs Group (RoofRepairs, Bricklaying, ConcreteRepair, DoorRepair, WindowGlassRepairs, FenceGateRepairs)
- DoorRepair → door type, emergency work
- ConcreteRepair/Bricklaying → area size, surface type
- FenceGateRepairs → repair type
• Locksmith → number of locks, lock types, delivery contact
• DrainDown → drain down lock required
• LetterboxSeal → fault description, block box fitted
• Cleaning Group (Cleaning, CleaningSolutions, DeepClean, InternalCleaners, DrugParaphernaliaClean)
→ clean type, areas to clean, deep clean required, biohazard cleaning
• Glazing/WindowGlassRepairs → window size, glass type, safety glass
• Painting Group (Painting, InteriorPainting, ExteriorPainting, DecoratingService)
→ areas to paint, paint color, number of coats
• GardenMaintenance → garden size, external work description
• Waste Group (WasteRemoval, HouseClearance, SiteClearance, CommercialPropertyClearance)
- HouseClearance → number of rooms to clean
- SiteClearance → waste type, volume estimate
• CCTVInstallation → number of cameras, recording equipment, remote access
• AccessControl → security equipment details, upgrade status, access areas
• Flooring → flooring type, area size
• Gas Safety (GasSafetyChecks, GasEngineers) → service type, licensed contractor
• AsbestosRemoval → known hazards, area of removal, permanent solution
• LeakInvestigation → location details
• VacantPropertyUpdate/VoidPropertyUpdate → services isolated, meter reading, fire alarm power
• Tiling → area to tile, tile type
• KeysafeInstallations → location and code
• IsolationOfServices → services to isolate
• ReconnectionVisits → services to reconnect
• EmergencyBoarding → immediate action required
• GraffitiRemoval → surface type, area size
• SolarPanelInstallation → number of panels, roof type
• FireAlarmService/AlarmRepairs → number of devices, system type
• LiftMaintenance → lift type, service frequency
• GutterRepair → repair extent
• KitchenInstallation/BathroomInstallation → items to install
• CarpentryService → work description
• WindowClean → window count, frequency
• LivestockPestClearance → infestation type, severity
2. Legal Tasks Section:
Condition: jobServiceCategory === 'Legal'
Shows: legalDetails.* and legalServiceDetails.*
Job-Type Specific Fields Within Legal:
• ProcessServing → document type, service deadline, urgent service, recipient details, max attempts
• DebtCollection → outstanding debt, debt type, months in arrears, payment history, bailiff details
• Tracing → subject aliases, last known location, relatives, employment, social media
• AssetTracing → asset types, known assets, estimated value, international search
• PreLitigation → claim type, claim amount, debt origin date, supporting documents
• Other Legal Types → general legal fields only
3. Security Tasks Section:
Condition: jobServiceCategory === 'Security'
Shows: securityDetails.* and securityServiceDetails.*
Job-Type Specific Fields Within Security:
• Patrol Group (MobilePatrol, StaticGuard, K9Patrol, K9DrugDetection, K9ExplosiveDetection, AlarmResponse)
- MobilePatrol → patrol frequency, route details
- StaticGuard → post location, shift times
- K9 Services → dog handler required, detection type
- AlarmResponse → response time required
• EventStaff → event type, expected attendance, special requirements
• CCTVMonitor → monitoring hours, number of screens
• Other Security Types → general security fields only
4. Utility Tasks Section:
Condition: jobServiceCategory === 'Utility'
Shows: meters[] array only
Meter Display Logic:
• Show if: meters && meters.length > 0
• For each meter show: product, serial, MPAN, type, size, location, last reading
• Availability indicator: if !meter.isAvailable show unavailable reason
Additional Visibility Rules:
• Urgent Attention: Show alert if requiresUrgentAttention === true
• Other Description: Show if otherJobTypeDescription exists
• Price: Show "TBD" if price === null or undefined
• Duration: Show estimated or "Not specified" if missing
• Rate Type: Show badge colored by Emergency/Premium/Standard
• Previous Visit Notes: Show blue info box if previousVisitNotes exists
• Admin Task: Show different UI if isAdminTask === true
Conditional Display Rules (Alert Boxes & Risk Cards):
1. Urgent Attention Required Card (Job Details Section)
| Condition |
Display |
Style |
Text |
requiresUrgentAttention == true OR priority == "Urgent" |
Show Card |
risk-card high (red) |
⚠️ Urgent Attention Required Badge: URGENT Text: "This job requires immediate action" |
priority == "High" |
Show Card |
risk-card medium (orange) |
⚠️ Priority Job Badge: PRIORITY Text: "Please prioritize this job" |
priority == "Normal" OR priority == "Low" |
Hide Card |
N/A |
N/A |
2. Site Hazards Card (Site Access Section)
Risk Level Calculation (Platform-Specific Code):
Swift (iOS)
// Determine risk level based on multiple factors
func calculateRiskLevel(propertyDetails: PropertyDetailsDto?,
ppeRequirements: String?) -> String {
guard let details = propertyDetails else { return "None" }
var riskLevel = "None"
if details.hasHealthAndSafetyRisks == true {
let notes = details.healthAndSafetyNotes?.lowercased() ?? ""
if notes.contains("high risk") ||
notes.contains("dangerous") ||
notes.contains("severe") ||
details.hasAsbestos == true ||
details.structuralIssues == true {
riskLevel = "High"
} else if details.requiresSpecialEquipment == true ||
(ppeRequirements != nil && ppeRequirements != "Standard PPE") {
riskLevel = "Medium"
} else {
riskLevel = "Low"
}
}
return riskLevel
}
// Usage in SwiftUI
var body: some View {
let riskLevel = calculateRiskLevel(
propertyDetails: subJob.clientJob.propertyDetails,
ppeRequirements: subJob.ppeRequirements
)
if riskLevel != "None" && riskLevel != "Low" {
RiskCard(level: riskLevel)
.padding()
}
}
Kotlin (Android)
// Determine risk level based on multiple factors
fun calculateRiskLevel(
propertyDetails: PropertyDetailsDto?,
ppeRequirements: String?
): String {
propertyDetails ?: return "None"
var riskLevel = "None"
if (propertyDetails.hasHealthAndSafetyRisks == true) {
val notes = propertyDetails.healthAndSafetyNotes?.lowercase() ?: ""
riskLevel = when {
notes.contains("high risk") ||
notes.contains("dangerous") ||
notes.contains("severe") ||
propertyDetails.hasAsbestos == true ||
propertyDetails.structuralIssues == true -> "High"
propertyDetails.requiresSpecialEquipment == true ||
(ppeRequirements != null && ppeRequirements != "Standard PPE") -> "Medium"
else -> "Low"
}
}
return riskLevel
}
// Usage in Compose
@Composable
fun SiteAccessSection(subJob: SubJobDetailDto) {
val riskLevel = calculateRiskLevel(
propertyDetails = subJob.clientJob?.propertyDetails,
ppeRequirements = subJob.ppeRequirements
)
if (riskLevel != "None" && riskLevel != "Low") {
RiskCard(
level = riskLevel,
modifier = Modifier.padding(16.dp)
)
}
}
Dart (Flutter)
// Determine risk level based on multiple factors
String calculateRiskLevel(
PropertyDetailsDto? propertyDetails,
String? ppeRequirements,
) {
if (propertyDetails == null) return 'None';
String riskLevel = 'None';
if (propertyDetails.hasHealthAndSafetyRisks == true) {
final notes = propertyDetails.healthAndSafetyNotes?.toLowerCase() ?? '';
if (notes.contains('high risk') ||
notes.contains('dangerous') ||
notes.contains('severe') ||
propertyDetails.hasAsbestos == true ||
propertyDetails.structuralIssues == true) {
riskLevel = 'High';
} else if (propertyDetails.requiresSpecialEquipment == true ||
(ppeRequirements != null && ppeRequirements != 'Standard PPE')) {
riskLevel = 'Medium';
} else {
riskLevel = 'Low';
}
}
return riskLevel;
}
// Usage in Flutter Widget
class SiteAccessSection extends StatelessWidget {
final SubJobDetailDto subJob;
@override
Widget build(BuildContext context) {
final riskLevel = calculateRiskLevel(
subJob.clientJob?.propertyDetails,
subJob.ppeRequirements,
);
if (riskLevel == 'None' || riskLevel == 'Low') {
return SizedBox.shrink();
}
return Padding(
padding: EdgeInsets.all(16),
child: RiskCard(level: riskLevel),
);
}
}
Java (Android)
// Determine risk level based on multiple factors
public String calculateRiskLevel(PropertyDetailsDto propertyDetails,
String ppeRequirements) {
if (propertyDetails == null) {
return "None";
}
String riskLevel = "None";
if (Boolean.TRUE.equals(propertyDetails.getHasHealthAndSafetyRisks())) {
String notes = propertyDetails.getHealthAndSafetyNotes();
if (notes == null) notes = "";
notes = notes.toLowerCase();
if (notes.contains("high risk") ||
notes.contains("dangerous") ||
notes.contains("severe") ||
Boolean.TRUE.equals(propertyDetails.getHasAsbestos()) ||
Boolean.TRUE.equals(propertyDetails.getStructuralIssues())) {
riskLevel = "High";
} else if (Boolean.TRUE.equals(propertyDetails.getRequiresSpecialEquipment()) ||
(ppeRequirements != null && !ppeRequirements.equals("Standard PPE"))) {
riskLevel = "Medium";
} else {
riskLevel = "Low";
}
}
return riskLevel;
}
// Usage in Activity/Fragment
private void setupSiteAccessSection(SubJobDetailDto subJob) {
PropertyDetailsDto propertyDetails = null;
if (subJob.getClientJob() != null) {
propertyDetails = subJob.getClientJob().getPropertyDetails();
}
String riskLevel = calculateRiskLevel(
propertyDetails,
subJob.getPpeRequirements()
);
View riskCard = findViewById(R.id.riskCard);
if ("None".equals(riskLevel) || "Low".equals(riskLevel)) {
riskCard.setVisibility(View.GONE);
} else {
riskCard.setVisibility(View.VISIBLE);
updateRiskCard(riskCard, riskLevel);
}
}
// Helper method to update risk card styling
private void updateRiskCard(View riskCard, String riskLevel) {
TextView riskBadge = riskCard.findViewById(R.id.riskBadge);
CardView cardView = (CardView) riskCard;
switch (riskLevel) {
case "High":
riskBadge.setText("HIGH RISK");
riskBadge.setBackgroundColor(Color.parseColor("#EF4444"));
cardView.setCardBackgroundColor(Color.parseColor("#1AEF4444"));
break;
case "Medium":
riskBadge.setText("MEDIUM RISK");
riskBadge.setBackgroundColor(Color.parseColor("#F59E0B"));
cardView.setCardBackgroundColor(Color.parseColor("#1AF59E0B"));
break;
default:
riskBadge.setText("LOW RISK");
riskBadge.setBackgroundColor(Color.parseColor("#10B981"));
cardView.setCardBackgroundColor(Color.parseColor("#1A10B981"));
}
}
| Risk Level |
Display |
Style |
Content |
| High |
Show Card |
risk-card high Border: red Background: rgba(239,68,68,0.05) |
⚠️ Site Hazards Badge: HIGH RISK Show all safety notes |
| Medium |
Show Card |
risk-card medium Border: orange Background: rgba(245,158,11,0.05) |
⚠️ Site Hazards Badge: MEDIUM RISK Show relevant notes |
| Low |
Hide Card |
N/A |
N/A |
| None |
Hide Card |
N/A |
N/A |
3. Legal Risk Assessment Card (Legal Tasks Section)
Risk Level Source: legalDetails.riskLevel
| Field Value |
Display |
Style |
Show Items |
| High |
Show Card |
risk-card high |
Violence Risk Safeguarding Concerns Risk Notes |
| Medium |
Show Card |
risk-card medium |
Relevant risks only |
| Low or null |
Hide Card |
N/A |
N/A |
4. Individual Field Visibility Rules
| Field |
Show When |
Hide When |
Style When Shown |
| Rate Type |
Always show |
Never |
Emergency/Urgent: Red badge Premium: Orange badge Standard: Plain text |
| Special Equipment |
requiresSpecialEquipment == true |
requiresSpecialEquipment == false/null |
⚠️ icon + equipment list |
| PPE Required |
ppeRequirements != null && != "Standard PPE" |
Standard or null |
Normal text (list items) |
| Pets on Site |
hasPetsOnSite == true |
hasPetsOnSite == false/null |
⚠️ icon + pet details |
| Previous Visit Notes |
previousVisitNotes != null |
null or empty |
Blue info box |
| Alarm System |
hasAlarm == true |
hasAlarm == false/null |
Show with code |
| Key Safe |
hasKeySafe == true |
hasKeySafe == false/null |
Location + Code (bold) |
5. Client Priority Tier Badge (Priority Bar)
| Client Tier |
Style |
Icon |
clientPriorityTier == "VIP" |
White bg, black border & text |
⭐ (bi-star-fill) |
clientPriorityTier == "Priority" |
Black bg, white text |
⭐ (bi-star-fill) |
clientPriorityTier == "Standard" |
Hide badge |
N/A |
6. Urgency Level Badge (Priority Bar)
| Urgency |
Background Color |
Icon & Text |
urgencyLevel == "Urgent" |
Red (#ef4444) |
⚡ (bi-lightning-fill) URGENT |
urgencyLevel == "Priority" |
Orange (#f59e0b) |
⚡ PRIORITY |
urgencyLevel == "Routine" |
Gray (#a3a3a3) |
ROUTINE |
7. Warning/Alert Icons Throughout UI
| Field Type |
Icon |
Color |
When Shown |
| Deadline/Time Sensitive |
⚠️ Warning emoji |
var(--urgent) |
SLA < 4 hours, Accept deadline approaching |
| Special Requirements |
⚠️ Warning emoji |
var(--warning) |
Special equipment, Pets, Non-standard PPE |
| Safety/Risk |
bi-exclamation-triangle |
var(--urgent) |
Violence risk, Health hazards |
| Protection/Security |
bi-shield-check |
var(--status-assigned) |
Safeguarding, Vulnerable party |
8. Document/Image Indicators
Mandatory Document Logic:
// A document is mandatory if ANY of these conditions are true:
function isDocumentMandatory(image: JobImageSummaryDto): boolean {
// 1. Explicitly marked as mandatory
if (image.isMandatory === true) return true;
// 2. Contains critical keywords in description
const description = image.description?.toLowerCase() || '';
const mandatoryKeywords = [
'property front', // Must identify property
'meter', // Meter readings/location
'damage', // Evidence of damage
'hazard', // Safety hazards
'before', // Before/after required
'certificate', // Certificates must be reviewed
'id', // ID verification
'court', // Legal documents
'warrant', // Legal authority
'notice' // Official notices
];
if (mandatoryKeywords.some(keyword => description.includes(keyword))) {
return true;
}
// 3. Job type specific requirements
const jobType = subJob.clientJob?.jobType;
const jobSpecificMandatory = {
'GasSafetyChecks': ['boiler', 'gas meter', 'certificate'],
'ProcessServing': ['recipient', 'property', 'proof'],
'MeterRead': ['meter', 'reading', 'display'],
'EmergencyBoarding': ['damage', 'before', 'after'],
'EvictionNotice': ['notice', 'property', 'posted']
};
if (jobSpecificMandatory[jobType]) {
return jobSpecificMandatory[jobType].some(
required => description.includes(required)
);
}
return false;
}
Swift Implementation:
func isDocumentMandatory(image: JobImageSummaryDto) -> Bool {
// 1. Explicitly marked as mandatory
if image.isMandatory == true { return true }
// 2. Contains critical keywords in description
let description = (image.description ?? "").lowercased()
let mandatoryKeywords = [
"property front", "meter", "damage", "hazard", "before",
"certificate", "id", "court", "warrant", "notice"
]
if mandatoryKeywords.contains(where: { description.contains($0) }) {
return true
}
// 3. Job type specific requirements
let jobType = subJob.clientJob?.jobType ?? ""
let jobSpecificMandatory: [String: [String]] = [
"GasSafetyChecks": ["boiler", "gas meter", "certificate"],
"ProcessServing": ["recipient", "property", "proof"],
"MeterRead": ["meter", "reading", "display"],
"EmergencyBoarding": ["damage", "before", "after"],
"EvictionNotice": ["notice", "property", "posted"]
]
if let requiredTerms = jobSpecificMandatory[jobType] {
return requiredTerms.contains { description.contains($0) }
}
return false
}
Kotlin Implementation:
fun isDocumentMandatory(image: JobImageSummaryDto): Boolean {
// 1. Explicitly marked as mandatory
if (image.isMandatory == true) return true
// 2. Contains critical keywords in description
val description = image.description?.toLowerCase() ?: ""
val mandatoryKeywords = listOf(
"property front", "meter", "damage", "hazard", "before",
"certificate", "id", "court", "warrant", "notice"
)
if (mandatoryKeywords.any { description.contains(it) }) {
return true
}
// 3. Job type specific requirements
val jobType = subJob.clientJob?.jobType ?: ""
val jobSpecificMandatory = mapOf(
"GasSafetyChecks" to listOf("boiler", "gas meter", "certificate"),
"ProcessServing" to listOf("recipient", "property", "proof"),
"MeterRead" to listOf("meter", "reading", "display"),
"EmergencyBoarding" to listOf("damage", "before", "after"),
"EvictionNotice" to listOf("notice", "property", "posted")
)
jobSpecificMandatory[jobType]?.let { requiredTerms ->
return requiredTerms.any { description.contains(it) }
}
return false
}
Dart/Flutter Implementation:
bool isDocumentMandatory(JobImageSummaryDto image) {
// 1. Explicitly marked as mandatory
if (image.isMandatory == true) return true;
// 2. Contains critical keywords in description
final description = (image.description ?? '').toLowerCase();
final mandatoryKeywords = [
'property front', 'meter', 'damage', 'hazard', 'before',
'certificate', 'id', 'court', 'warrant', 'notice'
];
if (mandatoryKeywords.any((keyword) => description.contains(keyword))) {
return true;
}
// 3. Job type specific requirements
final jobType = subJob.clientJob?.jobType ?? '';
final jobSpecificMandatory = {
'GasSafetyChecks': ['boiler', 'gas meter', 'certificate'],
'ProcessServing': ['recipient', 'property', 'proof'],
'MeterRead': ['meter', 'reading', 'display'],
'EmergencyBoarding': ['damage', 'before', 'after'],
'EvictionNotice': ['notice', 'property', 'posted']
};
if (jobSpecificMandatory.containsKey(jobType)) {
return jobSpecificMandatory[jobType]!
.any((required) => description.contains(required));
}
return false;
}
Java Implementation:
public boolean isDocumentMandatory(JobImageSummaryDto image) {
// 1. Explicitly marked as mandatory
if (Boolean.TRUE.equals(image.getIsMandatory())) return true;
// 2. Contains critical keywords in description
String description = (image.getDescription() != null ?
image.getDescription().toLowerCase() : "");
List<String> mandatoryKeywords = Arrays.asList(
"property front", "meter", "damage", "hazard", "before",
"certificate", "id", "court", "warrant", "notice"
);
if (mandatoryKeywords.stream().anyMatch(description::contains)) {
return true;
}
// 3. Job type specific requirements
String jobType = subJob.getClientJob() != null ?
subJob.getClientJob().getJobType() : "";
Map<String, List<String>> jobSpecificMandatory = new HashMap<>();
jobSpecificMandatory.put("GasSafetyChecks",
Arrays.asList("boiler", "gas meter", "certificate"));
jobSpecificMandatory.put("ProcessServing",
Arrays.asList("recipient", "property", "proof"));
jobSpecificMandatory.put("MeterRead",
Arrays.asList("meter", "reading", "display"));
jobSpecificMandatory.put("EmergencyBoarding",
Arrays.asList("damage", "before", "after"));
jobSpecificMandatory.put("EvictionNotice",
Arrays.asList("notice", "property", "posted"));
if (jobSpecificMandatory.containsKey(jobType)) {
return jobSpecificMandatory.get(jobType).stream()
.anyMatch(description::contains);
}
return false;
}
| Type |
Visual |
Implementation |
| Mandatory Document |
Red border + "!" badge |
isMandatory == true OR keyword match |
| Optional Document |
Normal gray border |
Default styling |
| PDF Document |
bi-file-pdf icon |
isPdf ? 'bi-file-pdf' : 'bi-image' |
Examples of Mandatory Documents by Job Type:
| Job Type |
Mandatory Documents |
| Gas Safety Checks |
Boiler location, Gas meter, Safety certificate |
| Process Serving |
Property front, Recipient ID, Proof of service |
| Meter Read |
Meter display, Serial number visible |
| Emergency Boarding |
Damage photos (before), Completed work (after) |
| Lock Change |
Old lock, New lock installed, Keys |
9. Section Priority Indicators
- Section with alerts: Red badge with count (e.g., Site Access shows "!" badge)
- Job Details section: Always expanded by default (
section-content active)
- Auto-decline bar: Red background, white text, always visible at top
10. Text Color Coding
| Content Type |
Color |
Examples |
| Urgent/Deadline |
var(--urgent) #ef4444 |
Accept By, Hearing Date, Service Deadline |
| Warning/Caution |
var(--warning) #f59e0b |
Pets on Site, Special Equipment |
| Success/Money |
var(--success) #10b981 |
Job Value (£85.00) |
| Links/Actions |
var(--status-assigned) #3b82f6 |
Phone numbers (callable) |
| Codes/Keys |
<strong> Bold |
Alarm codes, Key safe codes |
Important Notes:
- ✅ Accept: Call
POST /api/subjob/{id}/accept → Navigate to job-details-02-accepted view
- ✅ Decline: Call
POST /api/subjob/{id}/decline → Return to dashboard
- ✅ Countdown timer updates every second from
assignedAt + autoDeclineMinutes - now
- ⚠️ When timer reaches 0:00 → Navigate back to dashboard (backend handles auto-decline)