Job Management

API reference for job operations in the Zync SDK.

Table of contents

  1. Getting Jobs List
  2. Getting Dashboard Jobs
  3. Getting Job Details
    1. Parameters
    2. Return Value: GetJobResult
  4. Updating Job Status
  5. Real-time Job Updates
  6. Getting Job Statuses by Category
  7. Error Handling
    1. Common Error Types
  8. Offline-First Behavior
    1. Initial Sync vs Cached Data
    2. Background Delta Sync
    3. Network Resilience
  9. Best Practices
    1. Optimal Usage Patterns
      1. Efficient Job Loading
      2. Offline-First Operations
    2. Performance Guidelines
      1. Doโ€™s โœ…
      2. Donโ€™ts โŒ
    3. Error Handling Best Practices
    4. Security Considerations
  10. Filtering and Sorting Options
    1. Available Filter Options
    2. Sorting Capabilities

The ZyncJobManager provides comprehensive offline-first job management with real-time synchronization. Jobs represent work orders with complete customer information, assignments, assets, products, and status tracking. The manager follows the offline-first approach where local data is returned immediately and background sync operations keep the data updated.

Getting Jobs List

Retrieve a paginated list of jobs with comprehensive filtering and sorting capabilities:

import zync.api.job.models.GetJobsResult
import zync.api.job.models.ZyncJobSortAndFilter
import zync.api.job.models.ZyncJobSortBy
import zync.api.job.models.ZyncJobFilterDateRange
import zync.api.common.errors.ZyncError

// Create filter and sort criteria
val sortAndFilter = ZyncJobSortAndFilter(
    sortBy = ZyncJobSortBy.SCHEDULED_START_TIME,
    sortOrder = "DESC",
    keyword = "plumbing",
    categoryUid = "category_123",
    statusUids = listOf("status_456", "status_789"),
    priorityUids = listOf("priority_high"),
    assignedUserUids = listOf("user_abc"),
    dateRange = ZyncJobFilterDateRange(
        startDate = "2023-12-01T00:00:00Z",
        endDate = "2023-12-31T23:59:59Z"
    )
)

val result = zync.jobs.fetchJobs(
    sortAndFilter = sortAndFilter,
    page = 1,
    pageSize = 20
)

when (result) {
    is GetJobsResult.Success -> {
        val jobs = result.data
        println("Found ${jobs.size} jobs")
        println("Page ${result.currentPage} of ${result.totalPages}")
        println("Total records: ${result.totalRecords}")
        println("Is partial data: ${result.isPartialData}")

        jobs.forEach { job ->
            println("Job: ${job.jobTitle}")
            println("  Work Order: #${job.workOrderNumber}")
            println("  Status: ${job.currentStatus?.statusName ?: "Unknown"}")
            println("  Priority: ${job.jobPriority.priorityName}")
            println("  Scheduled: ${job.scheduledStartTime ?: "Not scheduled"}")

            // Customer information
            job.customer?.let { customer ->
                println("  Customer: ${customer.firstName} ${customer.lastName}")
                println("  Phone: ${customer.phoneNumber ?: "Not provided"}")
            }

            // Assignment information
            job.assignedUsers?.let { assignments ->
                if (assignments.isNotEmpty()) {
                    println("  Assigned to:")
                    assignments.forEach { assignment ->
                        println("    - ${assignment.user.displayName} (${assignment.team.teamName})")
                    }
                }
            }

            println("---")
        }
    }
    is GetJobsResult.Failure -> {
        println("Error: ${result.error.message}")
        when (result.error) {
            is ZyncError.Network -> {
                println("Check your internet connection")
            }
            is ZyncError.Error -> {
                result.error.code?.let { code ->
                    println("Error code: $code")
                }
            }
        }
    }
}
// Create filter and sort criteria
let sortAndFilter = ZyncJobSortAndFilter(
    sortBy: .scheduledStartTime,
    sortOrder: "DESC",
    keyword: "plumbing",
    categoryUid: "category_123",
    statusUids: ["status_456", "status_789"],
    priorityUids: ["priority_high"],
    assignedUserUids: ["user_abc"],
    dateRange: ZyncJobFilterDateRange(
        startDate: "2023-12-01T00:00:00Z",
        endDate: "2023-12-31T23:59:59Z"
    )
)

let result = try await zync.jobs.fetchJobs(
    sortAndFilter: sortAndFilter,
    page: 1,
    pageSize: 20
)

switch onEnum(of: result) {
case .success(let success):
    let jobs = success.data
    print("Found \(jobs.count) jobs")
    print("Page \(success.currentPage) of \(success.totalPages)")
    print("Total records: \(success.totalRecords)")
    print("Is partial data: \(success.isPartialData)")

    for job in jobs {
        print("Job: \(job.jobTitle)")
        print("  Work Order: #\(job.workOrderNumber)")
        print("  Status: \(job.currentStatus?.statusName ?? "Unknown")")
        print("  Priority: \(job.jobPriority.priorityName)")
        print("  Scheduled: \(job.scheduledStartTime ?? "Not scheduled")")

        // Customer information
        if let customer = job.customer {
            print("  Customer: \(customer.firstName ?? "") \(customer.lastName ?? "")")
            print("  Phone: \(customer.phoneNumber ?? "Not provided")")
        }

        // Assignment information
        if let assignments = job.assignedUsers, !assignments.isEmpty {
            print("  Assigned to:")
            for assignment in assignments {
                print("    - \(assignment.user.displayName ?? "") (\(assignment.team.teamName ?? ""))")
            }
        }

        print("---")
    }
case .failure(let failure):
    print("Error: \(failure.error.message)")
    switch onEnum(of: failure.error) {
    case .network:
        print("Check your internet connection")
    case .error(let error):
        print("Error: \(error.message)")
        if let code = error.code {
            print("Error code: \(code)")
        }
    }
}

Getting Dashboard Jobs

Retrieve jobs specifically formatted for dashboard display with optimized data structure:

import zync.api.job.models.GetJobsResult
import zync.api.job.models.ZyncJobSortAndFilter
import zync.api.job.models.ZyncJobFilterAssignmentStatus
import zync.api.job.models.ZyncJobFilterScheduleStatus

// Dashboard-specific filter for assigned jobs
val dashboardFilter = ZyncJobSortAndFilter(
    assignmentStatus = ZyncJobFilterAssignmentStatus.ASSIGNED_TO_ME,
    scheduleStatus = ZyncJobFilterScheduleStatus.TODAY,
    statusUids = listOf("in_progress", "scheduled")
)

val result = zync.jobs.fetchJobsForDashboard(
    sortAndFilter = dashboardFilter,
    page = 1,
    pageSize = 10
)

when (result) {
    is GetJobsResult.Success -> {
        val dashboardJobs = result.data
        println("Dashboard Jobs: ${dashboardJobs.size}")

        dashboardJobs.forEach { job ->
            println("๐Ÿ“‹ ${job.jobTitle}")
            println("   Priority: ${job.jobPriority.priorityName}")
            println("   Time: ${job.scheduledStartTime}")

            // Show sync status
            if (job.isSyncPending) {
                println("   ๐Ÿ”„ Sync pending")
            }

            job.customer?.let { customer ->
                println("   ๐Ÿ‘ค ${customer.firstName} ${customer.lastName}")
                customer.phoneNumber?.let { phone ->
                    println("   ๐Ÿ“ž $phone")
                }
            }
        }
    }
    is GetJobsResult.Failure -> {
        println("Failed to load dashboard jobs: ${result.error.message}")
    }
}
// Dashboard-specific filter for assigned jobs
let dashboardFilter = ZyncJobSortAndFilter(
    assignmentStatus: .assignedToMe,
    scheduleStatus: .today,
    statusUids: ["in_progress", "scheduled"]
)

let result = try await zync.jobs.fetchJobsForDashboard(
    sortAndFilter: dashboardFilter,
    page: 1,
    pageSize: 10
)

switch onEnum(of: result) {
case .success(let success):
    let dashboardJobs = success.data
    print("Dashboard Jobs: \(dashboardJobs.count)")

    for job in dashboardJobs {
        print("๐Ÿ“‹ \(job.jobTitle)")
        print("   Priority: \(job.jobPriority.priorityName)")
        print("   Time: \(job.scheduledStartTime ?? "")")

        // Show sync status
        if job.isSyncPending {
            print("   ๐Ÿ”„ Sync pending")
        }

        if let customer = job.customer {
            print("   ๐Ÿ‘ค \(customer.firstName ?? "") \(customer.lastName ?? "")")
            if let phone = customer.phoneNumber {
                print("   ๐Ÿ“ž \(phone)")
            }
        }
    }
case .failure(let failure):
    print("Failed to load dashboard jobs: \(failure.error.message)")
}

Getting Job Details

Retrieve comprehensive information for a specific job including all related data:

import zync.api.job.models.GetJobResult
import zync.api.common.errors.ZyncError

val result = zync.jobs.getJobDetail("a1b2c3d4-e5f6-7890-abcd-ef1234567890")

when (result) {
    is GetJobResult.Success -> {
        val job = result.data
        val syncStatus = result.syncStatus
        val lastSynced = result.lastSyncedAt

        println("Job Details:")
        println("Title: ${job.jobTitle}")
        println("Work Order: #${job.workOrderNumber}")
        println("Category: ${job.jobCategory?.categoryName ?: "Uncategorized"}")
        println("Priority: ${job.jobPriority.priorityName}")
        println("Timezone: ${job.jobTimezone ?: "Not specified"}")
        println("Description: ${job.jobDescription ?: "No description"}")

        if (syncStatus == ZyncDataSyncStatus.AGED) {
            println("Note: Data is older than 5 minutes. Last synced: $lastSynced")
        }

        // Scheduling information
        println("\nScheduling:")
        println("Start Time: ${job.scheduledStartTime ?: "Not scheduled"}")
        println("End Time: ${job.scheduledEndTime ?: "Not scheduled"}")
        println("Due Date: ${job.dueDate ?: "No due date"}")
        println("Recurring: ${if (job.isRecurringJob) "Yes" else "No"}")

        // Customer information
        job.customer?.let { customer ->
            println("\nCustomer:")
            println("Name: ${customer.firstName} ${customer.lastName}")
            println("Email: ${customer.emailAddress ?: "Not provided"}")
            println("Phone: ${customer.phoneNumber ?: "Not provided"}")

            // Customer feedback
            customer.customerFeedback?.let { feedback ->
                println("Rating: ${feedback.rating}/5")
                println("Comments: ${feedback.comments ?: "No comments"}")
            }
        }

        // Address information
        job.serviceAddress?.let { address ->
            println("\nService Address:")
            println("${address.address1}")
            address.address2?.let { println("${it}") }
            println("${address.city}, ${address.state} ${address.zipCode}")
            println("Country: ${address.country}")
        }

        // Status and history
        job.jobStatus?.let { status ->
            println("\nCurrent Status:")
            println("Status: ${status.statusName}")
            println("Type: ${status.statusType}")

            status.feedback?.let { feedback ->
                println("Feedback Required: ${feedback.isRequired}")
                println("Signature Required: ${feedback.isSignatureRequired}")
            }
        }

        // Status history
        job.statusHistory?.let { history ->
            if (history.isNotEmpty()) {
                println("\nStatus History:")
                history.forEach { entry ->
                    println("- ${entry.statusName} at ${entry.createdAt}")
                    println("  By: ${entry.doneBy.displayName}")
                    entry.remarks?.let { remarks ->
                        println("  Remarks: $remarks")
                    }
                }
            }
        }

        // Assigned users
        job.assignedUsers?.let { assignments ->
            if (assignments.isNotEmpty()) {
                println("\nAssigned Users:")
                assignments.forEach { assignment ->
                    println("- ${assignment.user.displayName}")
                    println("  Team: ${assignment.team.teamName}")
                    println("  Role: ${assignment.user.userRole ?: "Not specified"}")
                }
            }
        }

        // Assets
        job.assets?.let { assets ->
            if (assets.isNotEmpty()) {
                println("\nAssets:")
                assets.forEach { asset ->
                    println("- ${asset.assetName} (${asset.assetCode})")
                    println("  Quantity: ${asset.assetQuantity}")
                    println("  Status: ${asset.assetStatus}")
                    asset.assetSerialNumber?.let { serial ->
                        println("  Serial: $serial")
                    }
                }
            }
        }

        // Products
        job.products?.let { products ->
            if (products.isNotEmpty()) {
                println("\nProducts:")
                products.forEach { product ->
                    println("- ${product.productName}")
                    println("  Quantity: ${product.quantity}")
                    product.unitPrice?.let { price ->
                        println("  Unit Price: $price")
                    }
                }
            }
        }

        // Custom fields
        if (job.customFields.isNotEmpty()) {
            println("\nCustom Fields:")
            job.customFields.forEach { field ->
                println("- ${field.fieldLabel}: ${field.fieldValue ?: "Not filled"}")
            }
        }

        // Attachments
        if (job.attachments.isNotEmpty()) {
            println("\nAttachments:")
            job.attachments.forEach { attachment ->
                println("- ${attachment.originalFileName}")
                println("  Size: ${attachment.fileSizeInBytes} bytes")
                println("  Type: ${attachment.mimeType}")
            }
        }

        // Tags
        job.jobTags?.let { tags ->
            if (tags.isNotEmpty()) {
                println("\nTags: ${tags.joinToString(", ")}")
            }
        }
    }
    is GetJobResult.Failure -> {
        println("Error: ${result.error.message}")
        when (result.error) {
            is ZyncError.Network -> {
                println("Check your internet connection")
            }
            is ZyncError.Error -> {
                result.error.code?.let { code ->
                    when (code) {
                        404 -> println("Job not found")
                        403 -> println("Access denied")
                        else -> println("Error code: $code")
                    }
                }
            }
        }
    }
}
let result = try await zync.jobs.getJobDetail(jobUid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890")

switch onEnum(of: result) {
case .success(let success):
    let job = success.data
    let syncStatus = success.syncStatus
    let lastSynced = success.lastSyncedAt

    print("Job Details:")
    print("Title: \(job.jobTitle)")
    print("Work Order: #\(job.workOrderNumber)")
    print("Category: \(job.jobCategory?.categoryName ?? "Uncategorized")")
    print("Priority: \(job.jobPriority.priorityName)")
    print("Timezone: \(job.jobTimezone ?? "Not specified")")
    print("Description: \(job.jobDescription ?? "No description")")

    if syncStatus == .aged {
        print("Note: Data is older than 5 minutes. Last synced: \(lastSynced)")
    }

    // Scheduling information
    print("\nScheduling:")
    print("Start Time: \(job.scheduledStartTime ?? "Not scheduled")")
    print("End Time: \(job.scheduledEndTime ?? "Not scheduled")")
    print("Due Date: \(job.dueDate ?? "No due date")")
    print("Recurring: \(job.isRecurringJob ? "Yes" : "No")")

    // Customer information
    if let customer = job.customer {
        print("\nCustomer:")
        print("Name: \(customer.firstName ?? "") \(customer.lastName ?? "")")
        print("Email: \(customer.emailAddress ?? "Not provided")")
        print("Phone: \(customer.phoneNumber ?? "Not provided")")

        // Customer feedback
        if let feedback = customer.customerFeedback {
            print("Rating: \(feedback.rating)/5")
            print("Comments: \(feedback.comments ?? "No comments")")
        }
    }

    // Address information
    if let address = job.serviceAddress {
        print("\nService Address:")
        print("\(address.address1)")
        if let address2 = address.address2 {
            print("\(address2)")
        }
        print("\(address.city ?? ""), \(address.state ?? "") \(address.zipCode ?? "")")
        print("Country: \(address.country ?? "")")
    }

    // Status and history
    if let status = job.jobStatus {
        print("\nCurrent Status:")
        print("Status: \(status.statusName)")
        print("Type: \(status.statusType?.rawValue ?? "")")

        if let feedback = status.feedback {
            print("Feedback Required: \(feedback.isRequired)")
            print("Signature Required: \(feedback.isSignatureRequired)")
        }
    }

    // Status history
    if let history = job.statusHistory, !history.isEmpty {
        print("\nStatus History:")
        for entry in history {
            print("- \(entry.statusName) at \(entry.createdAt ?? "")")
            print("  By: \(entry.doneBy.displayName ?? "")")
            if let remarks = entry.remarks {
                print("  Remarks: \(remarks)")
            }
        }
    }

    // Assigned users
    if let assignments = job.assignedUsers, !assignments.isEmpty {
        print("\nAssigned Users:")
        for assignment in assignments {
            print("- \(assignment.user.displayName ?? "")")
            print("  Team: \(assignment.team.teamName ?? "")")
            print("  Role: \(assignment.user.userRole ?? "Not specified")")
        }
    }

    // Assets
    if let assets = job.assets, !assets.isEmpty {
        print("\nAssets:")
        for asset in assets {
            print("- \(asset.assetName) (\(asset.assetCode))")
            print("  Quantity: \(asset.assetQuantity)")
            print("  Status: \(asset.assetStatus?.rawValue ?? "")")
            if let serial = asset.assetSerialNumber {
                print("  Serial: \(serial)")
            }
        }
    }

    // Products
    if let products = job.products, !products.isEmpty {
        print("\nProducts:")
        for product in products {
            print("- \(product.productName)")
            print("  Quantity: \(product.quantity)")
            if let price = product.unitPrice {
                print("  Unit Price: \(price)")
            }
        }
    }

    // Custom fields
    if !job.customFields.isEmpty {
        print("\nCustom Fields:")
        for field in job.customFields {
            print("- \(field.fieldLabel): \(field.fieldValue ?? "Not filled")")
        }
    }

    // Attachments
    if !job.attachments.isEmpty {
        print("\nAttachments:")
        for attachment in job.attachments {
            print("- \(attachment.originalFileName)")
            print("  Size: \(attachment.fileSizeInBytes) bytes")
            print("  Type: \(attachment.mimeType)")
        }
    }

    // Tags
    if let tags = job.jobTags, !tags.isEmpty {
        print("\nTags: \(tags.joined(separator: ", "))")
    }
case .failure(let failure):
    print("Error: \(failure.error.message)")
    switch onEnum(of: failure.error) {
    case .network:
        print("Check your internet connection")
    case .error(let error):
        print("Error: \(error.message)")
        if let code = error.code {
            switch code {
            case 404:
                print("Job not found")
            case 403:
                print("Access denied")
            default:
                print("Error code: \(code)")
            }
        }
    }
}

Parameters

ParameterTypeDescription
jobUidStringUnique identifier of the job

Return Value: GetJobResult

Success Case:

  • data: ZyncJobDetail object with comprehensive job information
  • syncStatus: ZyncDataSyncStatus - Indicates the data freshness (NONE, AGED - older than 5 minutes, PARTIAL_RECORD, OUTDATED_RECORD)
  • lastSyncedAt: String - ISO-8601 formatted timestamp of when this record was last synced from the server

Failure Case:

  • error: ZyncError with error details

Updating Job Status

Update job status with offline-first support and comprehensive status tracking:

import zync.api.job.models.UpdateJobStatusResult
import zync.api.job.models.ZyncJobStatusUpdateContent
import zync.api.common.errors.ZyncError

// Simple status update
val statusUpdate = ZyncJobStatusUpdateContent(
    jobUid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    statusUid = "status_completed",
    remarks = "Job completed successfully",
    customerSignature = null,
    locationLatitude = 37.7749,
    locationLongitude = -122.4194
)

val result = zync.jobs.updateJobStatus(statusUpdate)

when (result) {
    is UpdateJobStatusResult.Success -> {
        println("Job status updated successfully")
        // Status is immediately available locally and will sync to server
    }
    is UpdateJobStatusResult.Failure -> {
        println("Failed to update status: ${result.error.message}")
        when (result.error) {
            is ZyncError.Network -> {
                println("Update saved locally, will sync when online")
            }
            is ZyncError.Error -> {
                result.error.code?.let { code ->
                    println("Error code: $code")
                }
            }
        }
    }
}

// Status update with customer signature
val statusWithSignature = ZyncJobStatusUpdateContent(
    jobUid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    statusUid = "status_completed",
    remarks = "Work completed to customer satisfaction",
    customerSignature = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...", // Base64 signature
    locationLatitude = 37.7749,
    locationLongitude = -122.4194
)

val signatureResult = zync.jobs.updateJobStatus(statusWithSignature)
// Simple status update
let statusUpdate = ZyncJobStatusUpdateContent(
    jobUid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    statusUid: "status_completed",
    remarks: "Job completed successfully",
    customerSignature: nil,
    locationLatitude: 37.7749,
    locationLongitude: -122.4194
)

let result = try await zync.jobs.updateJobStatus(content: statusUpdate)

switch onEnum(of: result) {
case .success:
    print("Job status updated successfully")
    // Status is immediately available locally and will sync to server
case .failure(let failure):
    print("Failed to update status: \(failure.error.message)")
    switch onEnum(of: failure.error) {
    case .network:
        print("Update saved locally, will sync when online")
    case .error(let error):
        print("Error: \(error.message)")
        if let code = error.code {
            print("Error code: \(code)")
        }
    }
}

// Status update with customer signature
let statusWithSignature = ZyncJobStatusUpdateContent(
    jobUid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    statusUid: "status_completed",
    remarks: "Work completed to customer satisfaction",
    customerSignature: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...", // Base64 signature
    locationLatitude: 37.7749,
    locationLongitude: -122.4194
)

let signatureResult = try await zync.jobs.updateJobStatus(content: statusWithSignature)

Real-time Job Updates

Monitor job changes in real-time using the reactive Flow API for immediate UI updates:

import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.collect
import zync.api.job.models.JobChangeEvent

// Observe changes for a specific job
val job = launch {
    zync.jobs.observeJobChanges("a1b2c3d4-e5f6-7890-abcd-ef1234567890").collect { changeEvent ->
        when (changeEvent) {
            is JobChangeEvent.JobUpdated -> {
                val updatedJobs = changeEvent.updatedJobs
                val deletedJobUids = changeEvent.deletedJobUids

                // Handle deletions first
                deletedJobUids.forEach { deletedUid ->
                    currentJobList.removeAll { it.jobUid == deletedUid }
                    println("Removed deleted job: $deletedUid")
                }

                // Handle updates and additions
                updatedJobs.forEach { updatedJob ->
                    val existingIndex = currentJobList.indexOfFirst {
                        it.jobUid == updatedJob.jobUid
                    }
                    if (existingIndex != -1) {
                        // Update existing job
                        currentJobList[existingIndex] = updatedJob
                        println("Updated job: ${updatedJob.jobTitle}")

                        // Check for specific changes
                        if (updatedJob.isSyncPending) {
                            showSyncIndicator(updatedJob.jobUid)
                        }

                        // Check status changes
                        updatedJob.jobStatus?.let { status ->
                            showStatusUpdate("Job status changed to ${status.statusName}")
                        }
                    } else {
                        // Add new job
                        currentJobList.add(updatedJob)
                        println("Added new job: ${updatedJob.jobTitle}")
                    }
                }

                // Show notification for changes
                val totalChanges = updatedJobs.size + deletedJobUids.size
                if (totalChanges > 0) {
                    showUpdateAvailableChip("$totalChanges job changes available")
                }

                // Refresh UI with updated list
                notifyJobListChanged()
            }
        }
    }
}

// Cancel when done
job.cancel()
// Observe changes for a specific job using SKIE's AsyncSequence support
let task = Task {
    for await changeEvent in zync.jobs.observeJobChanges(jobUid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890") {
        switch changeEvent {
        case .jobUpdated(let updateEvent):
            let updatedJobs = updateEvent.updatedJobs
            let deletedJobUids = updateEvent.deletedJobUids

            // Handle deletions first
            for deletedUid in deletedJobUids {
                currentJobList.removeAll { $0.jobUid == deletedUid }
                print("Removed deleted job: \(deletedUid)")
            }

            // Handle updates and additions
            for updatedJob in updatedJobs {
                if let existingIndex = currentJobList.firstIndex(where: {
                    $0.jobUid == updatedJob.jobUid
                }) {
                    // Update existing job
                    currentJobList[existingIndex] = updatedJob
                    print("Updated job: \(updatedJob.jobTitle)")

                    // Check for specific changes
                    if updatedJob.isSyncPending {
                        await showSyncIndicator(updatedJob.jobUid)
                    }

                    // Check status changes
                    if let status = updatedJob.jobStatus {
                        await showStatusUpdate("Job status changed to \(status.statusName)")
                    }
                } else {
                    // Add new job
                    currentJobList.append(updatedJob)
                    print("Added new job: \(updatedJob.jobTitle)")
                }
            }

            // Show notification for changes
            let totalChanges = updatedJobs.count + deletedJobUids.count
            if totalChanges > 0 {
                await showUpdateAvailableChip("\(totalChanges) job changes available")
            }

            // Refresh UI with updated list
            await notifyJobListChanged()
        }
    }
}

// Cancel when done
task.cancel()

Getting Job Statuses by Category

Retrieve available status options for a specific job category:

import zync.api.job.models.GetJobStatusesByCategoryResult
import zync.api.common.errors.ZyncError

val result = zync.jobs.getJobStatusesByCategory("category_uid_123")

when (result) {
    is GetJobStatusesByCategoryResult.Success -> {
        val statuses = result.data
        println("Available statuses for category:")

        statuses.forEach { status ->
            println("Status: ${status.statusName}")
            println("  Type: ${status.statusType}")
            println("  Color: ${status.statusColor}")
            println("  Sequence: ${status.sequenceNo}")

            // Check if feedback is required
            status.feedback?.let { feedback ->
                println("  Feedback Required: ${feedback.isRequired}")
                println("  Signature Required: ${feedback.isSignatureRequired}")

                if (feedback.isRequired) {
                    println("  Feedback Fields:")
                    feedback.feedbackFields?.forEach { field ->
                        println("    - ${field.fieldLabel} (${field.fieldType})")
                        if (field.isRequired) {
                            println("      * Required")
                        }
                    }
                }
            }

            // Check for checklist items
            status.checklist?.let { checklist ->
                if (checklist.isNotEmpty()) {
                    println("  Checklist (${checklist.size} items):")
                    checklist.forEach { item ->
                        println("    โ˜ ${item.checklistItem}")
                        if (item.isRequired) {
                            println("      * Required")
                        }
                    }
                }
            }

            println("---")
        }
    }
    is GetJobStatusesByCategoryResult.Failure -> {
        println("Error: ${result.error.message}")
        when (result.error) {
            is ZyncError.Network -> {
                println("Check your internet connection")
            }
            is ZyncError.Error -> {
                result.error.code?.let { code ->
                    println("Error code: $code")
                }
            }
        }
    }
}
let result = try await zync.jobs.getJobStatusesByCategory(categoryUid: "category_uid_123")

switch onEnum(of: result) {
case .success(let success):
    let statuses = success.data
    print("Available statuses for category:")

    for status in statuses {
        print("Status: \(status.statusName)")
        print("  Type: \(status.statusType?.rawValue ?? "")")
        print("  Color: \(status.statusColor)")
        print("  Sequence: \(status.sequenceNo)")

        // Check if feedback is required
        if let feedback = status.feedback {
            print("  Feedback Required: \(feedback.isRequired)")
            print("  Signature Required: \(feedback.isSignatureRequired)")

            if feedback.isRequired {
                print("  Feedback Fields:")
                if let fields = feedback.feedbackFields {
                    for field in fields {
                        print("    - \(field.fieldLabel) (\(field.fieldType))")
                        if field.isRequired {
                            print("      * Required")
                        }
                    }
                }
            }
        }

        // Check for checklist items
        if let checklist = status.checklist, !checklist.isEmpty {
            print("  Checklist (\(checklist.count) items):")
            for item in checklist {
                print("    โ˜ \(item.checklistItem)")
                if item.isRequired {
                    print("      * Required")
                }
            }
        }

        print("---")
    }
case .failure(let failure):
    print("Error: \(failure.error.message)")
    switch onEnum(of: failure.error) {
    case .network:
        print("Check your internet connection")
    case .error(let error):
        print("Error: \(error.message)")
        if let code = error.code {
            print("Error code: \(code)")
        }
    }
}

Error Handling

Common Error Types

when (result) {
    is GetJobsResult.Success -> {
        // Handle success
        val jobs = result.data
        displayJobs(jobs)
    }
    is GetJobsResult.Failure -> {
        when (result.error) {
            is ZyncError.Network -> {
                println("Network error - showing cached data")
                // Show offline indicator but continue with cached data
                showOfflineIndicator()
                loadCachedJobs()
            }
            is ZyncError.Error -> {
                println("API error: ${result.error.message}")
                result.error.code?.let { code ->
                    when (code) {
                        401 -> showLoginPrompt()
                        403 -> showAccessDeniedMessage()
                        404 -> showNoJobsMessage()
                        else -> showGenericErrorMessage()
                    }
                }
            }
        }
    }
}
switch onEnum(of: result) {
case .success(let success):
    // Handle success
    let jobs = success.data
    displayJobs(jobs)
case .failure(let failure):
    switch onEnum(of: failure.error) {
    case .network:
        print("Network error - showing cached data")
        // Show offline indicator but continue with cached data
        showOfflineIndicator()
        loadCachedJobs()
    case .error(let error):
        print("API error: \(error.message)")
        if let code = error.code {
            switch code {
            case 401:
                showLoginPrompt()
            case 403:
                showAccessDeniedMessage()
            case 404:
                showNoJobsMessage()
            default:
                showGenericErrorMessage()
            }
        }
    }
}

Offline-First Behavior

The Zync SDK implements a sophisticated offline-first pattern for job operations, ensuring optimal performance and user experience regardless of network conditions.

Initial Sync vs Cached Data

First Time Access:

  • Performs synchronous initial sync from server
  • Creates sync metadata to track sync state
  • Returns complete data set after successful sync
  • Marks initial sync as complete for future optimizations

Subsequent Access:

  • Returns cached data immediately (no waiting)
  • Triggers background delta sync for updated data
  • Updates cache silently without blocking user interface
  • Uses last sync timestamp to fetch only changed jobs

Background Delta Sync

// First call - performs initial sync
val initialResult = zync.jobs.fetchJobs(
    sortAndFilter = jobFilter,
    page = 1,
    pageSize = 20
)
// User waits for sync to complete

// Subsequent calls - immediate response + background sync
val cachedResult = zync.jobs.fetchJobs(
    sortAndFilter = jobFilter,
    page = 1,
    pageSize = 20
)
// User gets immediate response with cached data
// SDK performs background delta sync for any updates since last sync
// First call - performs initial sync
let initialResult = try await zync.jobs.fetchJobs(
    sortAndFilter: jobFilter,
    page: 1,
    pageSize: 20
)
// User waits for sync to complete

// Subsequent calls - immediate response + background sync
let cachedResult = try await zync.jobs.fetchJobs(
    sortAndFilter: jobFilter,
    page: 1,
    pageSize: 20
)
// User gets immediate response with cached data
// SDK performs background delta sync for any updates since last sync

Network Resilience

Offline Operations:

  • All status updates work offline and queue for sync
  • Local database serves as single source of truth
  • Changes are preserved and synced when connectivity returns

Online Operations:

  • Background sync runs automatically without blocking UI
  • Smart conflict resolution for concurrent modifications
  • Automatic retry with exponential backoff for failed operations

Performance Benefit: After initial sync, users get instant responses while the SDK keeps data fresh in the background. This provides the best of both worlds - immediate responsiveness and up-to-date job data.

Best Practices

Optimal Usage Patterns

Efficient Job Loading

class JobViewModel {
    private var cachedJobs: List<ZyncJobCompact> = emptyList()
    private var isInitialLoad = true

    suspend fun loadJobs(filter: ZyncJobSortAndFilter) {
        val result = zync.jobs.fetchJobs(
            sortAndFilter = filter,
            page = 1,
            pageSize = 20
        )

        when (result) {
            is GetJobsResult.Success -> {
                cachedJobs = result.data
                // First load takes longer (initial sync)
                // Subsequent loads are instant (cached + background sync)
                isInitialLoad = false
                updateUI(result.data, result.isPartialData)
            }
            is GetJobsResult.Failure -> {
                handleError(result.error)
            }
        }
    }

    // Use real-time updates for better UX
    suspend fun observeJobChanges(jobUid: String) {
        zync.jobs.observeJobChanges(jobUid).collect { event ->
            updateJobInUI(event)
        }
    }
}

Offline-First Operations

// Update job status offline - it'll sync automatically
suspend fun completeJob(jobUid: String, remarks: String, signature: String?) {
    val statusUpdate = ZyncJobStatusUpdateContent(
        jobUid = jobUid,
        statusUid = "status_completed",
        remarks = remarks,
        customerSignature = signature,
        locationLatitude = getCurrentLocation()?.latitude,
        locationLongitude = getCurrentLocation()?.longitude
    )

    val result = zync.jobs.updateJobStatus(statusUpdate)
    // Status is immediately available locally
    // Will sync to server when connectivity is available
}

Performance Guidelines

Doโ€™s โœ…

  • Let the SDK manage sync timing automatically
  • Use cached data for immediate UI responsiveness
  • Update job status optimistically (offline-first)
  • Use real-time updates for keeping UI fresh
  • Embrace background sync for keeping data current

Donโ€™ts โŒ

  • Donโ€™t force manual sync unless absolutely necessary
  • Donโ€™t ignore offline capabilities - embrace offline-first patterns
  • Donโ€™t block UI while waiting for sync operations

Error Handling Best Practices

suspend fun handleJobOperations() {
    try {
        val result = zync.jobs.fetchJobs(filter, page, pageSize)
        when (result) {
            is GetJobsResult.Success -> {
                // Handle success
                displayJobs(result.data)
            }
            is GetJobsResult.Failure -> {
                when (result.error) {
                    is ZyncError.Network -> {
                        // Show offline indicator but continue with cached data
                        showOfflineIndicator()
                        loadCachedData()
                    }
                    is ZyncError.Error -> {
                        // Log error and show user-friendly message
                        logError(result.error)
                        showErrorMessage("Unable to sync jobs. Using cached data.")
                        loadCachedData()
                    }
                }
            }
        }
    } catch (exception: Exception) {
        // Handle unexpected errors gracefully
        logError(exception)
        showGenericErrorMessage()
    }
}

Security Considerations

  • Offline Security: Job data is stored securely in encrypted local database
  • Sync Security: All network operations use secure connections
  • Data Privacy: Customer and assignment data are handled according to privacy settings

Key Takeaway: Embrace the offline-first pattern. The SDK is designed to work seamlessly offline with automatic sync - donโ€™t fight this design by forcing online operations.

Filtering and Sorting Options

Available Filter Options

The job filtering system provides comprehensive options for finding relevant jobs:

  • Keyword Search: Search in job titles, descriptions, and work order numbers
  • Category Filtering: Filter by job category UID
  • Status Filtering: Multiple status UIDs for flexible status-based filtering
  • Priority Filtering: Filter by priority levels (high, medium, low)
  • Assignment Filtering: Filter by assigned users or assignment status
  • Date Range Filtering: Schedule-based date filtering
  • Location Filtering: Proximity-based filtering (when location data available)

Sorting Capabilities

Jobs can be sorted by multiple criteria:

  • Scheduled Start Time: Most common for schedule-based views
  • Created Date: For chronological organization
  • Priority: For priority-based task management
  • Work Order Number: For reference-based organization
  • Customer Name: For customer-centric views

Note: All filtering and sorting operations work offline using cached data and are optimized for mobile performance.


Copyright © 2025 Zuper Inc. All rights reserved. This software is proprietary and confidential.