Job Management
API reference for job operations in the Zync SDK.
Table of contents
- Getting Jobs List
- Getting Dashboard Jobs
- Getting Job Details
- Updating Job Status
- Real-time Job Updates
- Getting Job Statuses by Category
- Error Handling
- Offline-First Behavior
- Best Practices
- Filtering and Sorting Options
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
| Parameter | Type | Description |
|---|---|---|
jobUid | String | Unique identifier of the job |
Return Value: GetJobResult
Success Case:
data:ZyncJobDetailobject with comprehensive job informationsyncStatus: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:ZyncErrorwith 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.