🔧 DEV MODE - Hover over orange-bordered elements to see field mappings from SubJob/ClientJob DTOs
← Back to Index
NEW JOB - AUTO-DECLINE IN
28:45
URGENT PRIORITY CLIENT

Gas Safety Check & Meter Reading

#GSC-2024-1862 · British Gas
Today, 4:00 PM - 6:00 PM
Est. 45 minutes
3.2 miles away
£85.00
Job Payment
Local Job Category
Standard Rate Type
4 hours SLA Deadline
Performance Impact
89%
Current Rate
90%
If Accepted
+1%
87%
If Declined
-2%
£500
Bonus at Risk
Distance
3.2 miles · 12 min
Navigate
42 Manchester Road
Birmingham, West Midlands
B15 2JK
United Kingdom
Travel
12 min
3.2 miles
Job Details
⚠️ Urgent Attention Required URGENT
This job requires immediate action
Job Type {{subJob.clientJob.jobType || 'N/A'}}
Service Category {{subJob.clientJob.jobServiceCategory || 'N/A'}}
Client {{subJob.clientJob.clientName || ''}}
Location {{subJob.clientJob.customerAddress || 'N/A'}}, {{subJob.clientJob.customerPostcode || 'N/A'}}
Priority {{subJob.priority || 'Normal'}}
Completion By {{(subJob.clientJob.requiredCompletionDate | date:'shortDate') || 'N/A'}}
Our Reference {{subJob.ourJobReference || 'N/A'}}
Description {{subJob.clientJob.otherJobTypeDescription}}
Customer Info
{{subJob.clientJob.customerName || 'N/A'}}
{{subJob.clientJob.companyName || 'Customer'}}
{{subJob.clientJob.customerTelephone || subJob.clientJob.customerMobile || 'N/A'}}
Email {{subJob.clientJob.customerEmail || 'N/A'}}
Preferred Contact {{subJob.clientJob.preferredContactMethod || 'N/A'}} {{subJob.clientJob.preferredContactTime ? '(' + subJob.clientJob.preferredContactTime + ')' : ''}}
Helpdesk {{subJob.clientJob.clientHelpdeskPhone || 'N/A'}}
Customer Ref {{subJob.clientJob.customerReference || 'N/A'}}
Alt Contacts {{subJob.clientJob.alternativeContacts}}
Tenant {{subJob.clientJob.propertyDetails?.tenantContactName}} - {{subJob.clientJob.propertyDetails?.tenantContactNumber || 'N/A'}}
Site Access !
⚠️ Site Hazards HIGH RISK
Health & Safety Risks: {{subJob.clientJob.propertyDetails?.healthAndSafetyNotes || 'Check on arrival'}}
Asbestos: {{subJob.clientJob.propertyDetails?.asbestosDetails || 'Present'}}
Pest Infestation: {{subJob.clientJob.propertyDetails?.pestDetails || 'Active'}}
Parking {{subJob.clientJob.parkingType || 'Street parking'}}
Key Holder {{subJob.clientJob.propertyDetails?.keyHolderName}} - {{subJob.clientJob.propertyDetails?.keyHolderContact || 'N/A'}}
Key Safe Location: {{subJob.clientJob.propertyDetails?.keySafeLocation}}
Code: {{subJob.clientJob.propertyDetails?.keySafeCode || 'N/A'}}
Alarm System Yes - Code: {{subJob.clientJob.propertyDetails?.alarmCode || 'N/A'}}
Access Instructions {{subJob.clientJob.propertyDetails?.accessInstructions || 'Standard access'}}
Pets on Site ⚠️ Yes - {{subJob.clientJob.petDetails || 'Details not specified'}}
Occupancy {{subJob.clientJob.propertyDetails?.occupancyStatus || 'N/A'}}
Tenant Available {{subJob.clientJob.propertyDetails?.tenantAvailability || 'N/A'}}
Work Instructions
📝 Previous Visit Notes
{{subJob.previousVisitNotes}}
Visit Reason {{subJob.clientJob.visitReason || 'N/A'}}
Requirements {{subJob.clientJob.additionalRequirements || 'N/A'}}
Est. Duration {{subJob.estimatedDurationMinutes || '45'}} minutes
PPE Required {{subJob.ppeRequirements || 'Standard PPE'}}
Special Equipment ⚠️ Yes - {{subJob.equipmentNotes || 'Check requirements'}}
Prerequisites ℹ️ Check before starting
Notes {{subJob.notes}}
Rate Type {{subJob.rateType}}
Job Value {{subJob.price | currency:'GBP'}}
Images ({{(subJob.clientJob.groupImages ? subJob.clientJob.groupImages.length : 0) + (subJob.clientJob.jobImages ? subJob.clientJob.jobImages.length : 0)}})
!
{{doc.description || 'Document'}}
{{doc.uploadedAt | date:'short'}}

No images attached

Schedule
Assigned At {{(subJob.assignedAt | date:'short') || 'Just now'}}
Accept By {{(subJob.acceptanceDeadline | date:'short') || 'ASAP'}}
Scheduled Date {{(subJob.scheduledDate | date:'shortDate') || 'Today'}}
Scheduled Time {{formatTime(subJob.scheduledTime) || 'ASAP'}}
Completed On {{(subJob.completionDate | date:'shortDate')}} {{formatTime(subJob.completionTime)}}

🔧 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

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: