Timelog Management

API reference for timelog operations in the Zync SDK.

Table of contents

  1. Getting Job Timelog Summary
  2. Getting Active Timelog
  3. Updating Timelog Entries
    1. Timelog Types and Auto-Entry Logic
  4. Error Handling
    1. Common Error Types
  5. Offline-First Behavior
    1. Timelog Data Access
    2. Offline Timelog Operations
    3. Network Resilience
  6. Best Practices
    1. Optimal Usage Patterns
      1. Efficient Timelog Management
      2. Offline-First Operations
    2. Performance Guidelines
      1. Do’s ✅
      2. Don’ts ❌
    3. Error Handling Best Practices
    4. Security Considerations
  7. Time Calculation Features
    1. Multi-Activity Support
    2. Robust Error Handling
    3. Summary Calculation

The ZyncTimelogManager provides comprehensive offline-first timelog management with real-time synchronization. Timelogs track work time, break time, and travel time with complete audit trails and automatic state transitions. The manager follows the offline-first approach where local data is returned immediately and background sync operations keep the data updated.

Getting Job Timelog Summary

Retrieve comprehensive timelog summary for a specific job with detailed time calculations and individual entries:

import zync.api.timelog.models.GetJobTimeLogSummaryResult
import zync.api.common.errors.ZyncError

val result = zync.timelog.getJobTimeLogSummary(
    jobUid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    isTimeLogV2Enabled = true
)

when (result) {
    is GetJobTimeLogSummaryResult.Success -> {
        val summary = result.data
        println("Job: ${summary.jobUid}")
        println("Total Time: ${summary.totalTime} minutes")
        println("Work Time: ${summary.totalWorkTime} minutes")
        println("Break Time: ${summary.totalBreakTime} minutes")
        println("Travel Time: ${summary.totalTravelTime} minutes")
        println("User: ${summary.user.firstName} ${summary.user.lastName}")

        // Display individual timelog entries
        summary.entries.forEach { entry ->
            println("Entry: ${entry.type} at ${entry.checkedTime}")
            println("  Type: ${entry.timeLogType}")
            println("  Location: ${entry.latitude}, ${entry.longitude}")
            entry.remarks?.let { remarks ->
                println("  Remarks: $remarks")
            }
        }

        // Display computed summary sessions
        summary.timeLogSummaryEntries.forEach { session ->
            println("Session: ${session.timeLogType}")
            println("  Time Spent: ${session.timeSpent} minutes")
            println("  Start: ${session.startTime}")
            println("  End: ${session.endTime ?: "Ongoing"}")
        }
    }
    is GetJobTimeLogSummaryResult.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.timelog.getJobTimeLogSummary(
    jobUid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    isTimeLogV2Enabled: true
)

switch onEnum(of: result) {
case .success(let success):
    let summary = success.data
    print("Job: \(summary.jobUid)")
    print("Total Time: \(summary.totalTime) minutes")
    print("Work Time: \(summary.totalWorkTime) minutes")
    print("Break Time: \(summary.totalBreakTime) minutes")
    print("Travel Time: \(summary.totalTravelTime) minutes")
    print("User: \(summary.user.firstName ?? "") \(summary.user.lastName ?? "")")

    // Display individual timelog entries
    for entry in summary.entries {
        print("Entry: \(entry.type) at \(entry.checkedTime ?? "")")
        print("  Type: \(entry.timeLogType ?? "")")
        if let lat = entry.latitude, let lng = entry.longitude {
            print("  Location: \(lat), \(lng)")
        }
        if let remarks = entry.remarks {
            print("  Remarks: \(remarks)")
        }
    }

    // Display computed summary sessions
    for session in summary.timeLogSummaryEntries {
        print("Session: \(session.timeLogType ?? "")")
        print("  Time Spent: \(session.timeSpent) minutes")
        print("  Start: \(session.startTime ?? "")")
        print("  End: \(session.endTime ?? "Ongoing")")
    }
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 Active Timelog

Retrieve the current active timelog entry to check if the user is currently clocked in:

import zync.api.timelog.models.GetActiveTimeLogResult
import zync.api.common.errors.ZyncError

val result = zync.timelog.getActiveTimeLog()

when (result) {
    is GetActiveTimeLogResult.Success -> {
        val activeTimeLog = result.data
        if (activeTimeLog != null) {
            println("Active Timelog: ${activeTimeLog.timeLogUid}")
            println("Job: ${activeTimeLog.jobUid}")
            println("Type: ${activeTimeLog.timeLogType}")
            println("Started: ${activeTimeLog.checkedTime}")
            println("User: ${activeTimeLog.user.displayName}")

            // Check location if available
            activeTimeLog.latitude?.let { lat ->
                activeTimeLog.longitude?.let { lng ->
                    println("Location: $lat, $lng")
                }
            }

            activeTimeLog.remarks?.let { remarks ->
                println("Remarks: $remarks")
            }
        } else {
            println("No active timelog - user is not clocked in")
        }
    }
    is GetActiveTimeLogResult.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.timelog.getActiveTimeLog()

switch onEnum(of: result) {
case .success(let success):
    if let activeTimeLog = success.data {
        print("Active Timelog: \(activeTimeLog.timeLogUid)")
        print("Job: \(activeTimeLog.jobUid)")
        print("Type: \(activeTimeLog.timeLogType ?? "")")
        print("Started: \(activeTimeLog.checkedTime ?? "")")
        print("User: \(activeTimeLog.user.displayName ?? "")")

        // Check location if available
        if let lat = activeTimeLog.latitude, let lng = activeTimeLog.longitude {
            print("Location: \(lat), \(lng)")
        }

        if let remarks = activeTimeLog.remarks {
            print("Remarks: \(remarks)")
        }
    } else {
        print("No active timelog - user is not clocked in")
    }
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)")
        }
    }
}

Updating Timelog Entries

Create or update timelog entries with comprehensive offline-first support and automatic state transitions:

import zync.api.timelog.models.UpdateTimelogRequest
import zync.api.timelog.models.UpdateTimelogResult
import zync.api.common.errors.ZyncError

// Clock in for work
val clockInRequest = UpdateTimelogRequest(
    type = "CLOCK_IN",
    latitude = 37.7749,
    longitude = -122.4194,
    checkedTime = "2023-12-07T09:00:00Z",
    jobUid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    timelogType = "JOB",
    remarks = "Started work on customer site",
    laborCodeUid = "labor_code_123"
)

val clockInResult = zync.timelog.updateTimelog(clockInRequest)

when (clockInResult) {
    is UpdateTimelogResult.Success -> {
        println("Successfully clocked in for work")
        // Entry is immediately available locally and will sync to server
    }
    is UpdateTimelogResult.Failure -> {
        println("Failed to clock in: ${clockInResult.error.message}")
        when (clockInResult.error) {
            is ZyncError.Network -> {
                println("Saved locally, will sync when online")
            }
            is ZyncError.Error -> {
                clockInResult.error.code?.let { code ->
                    println("Error code: $code")
                }
            }
        }
    }
}

// Clock out with meal break
val clockOutRequest = UpdateTimelogRequest(
    type = "CLOCK_OUT",
    latitude = 37.7749,
    longitude = -122.4194,
    checkedTime = "2023-12-07T12:00:00Z",
    jobUid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    timelogType = "JOB",
    reasonCode = "MEAL_BREAK",
    remarks = "Going for lunch break"
)

val clockOutResult = zync.timelog.updateTimelog(clockOutRequest)

// Travel entry
val travelRequest = UpdateTimelogRequest(
    type = "CLOCK_IN",
    latitude = 37.7849,
    longitude = -122.4094,
    checkedTime = "2023-12-07T14:00:00Z",
    jobUid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    timelogType = "TRAVEL",
    remarks = "Traveling to next job site"
)

val travelResult = zync.timelog.updateTimelog(travelRequest)
// Clock in for work
let clockInRequest = UpdateTimelogRequest(
    type: "CLOCK_IN",
    latitude: 37.7749,
    longitude: -122.4194,
    checkedTime: "2023-12-07T09:00:00Z",
    projectUid: nil,
    jobUid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    timelogType: "JOB",
    reasonCode: nil,
    remarks: "Started work on customer site",
    laborCodeUid: "labor_code_123"
)

let clockInResult = try await zync.timelog.updateTimelog(request: clockInRequest)

switch onEnum(of: clockInResult) {
case .success:
    print("Successfully clocked in for work")
    // Entry is immediately available locally and will sync to server
case .failure(let failure):
    print("Failed to clock in: \(failure.error.message)")
    switch onEnum(of: failure.error) {
    case .network:
        print("Saved locally, will sync when online")
    case .error(let error):
        print("Error: \(error.message)")
        if let code = error.code {
            print("Error code: \(code)")
        }
    }
}

// Clock out with meal break
let clockOutRequest = UpdateTimelogRequest(
    type: "CLOCK_OUT",
    latitude: 37.7749,
    longitude: -122.4194,
    checkedTime: "2023-12-07T12:00:00Z",
    projectUid: nil,
    jobUid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    timelogType: "JOB",
    reasonCode: "MEAL_BREAK",
    remarks: "Going for lunch break",
    laborCodeUid: nil
)

let clockOutResult = try await zync.timelog.updateTimelog(request: clockOutRequest)

// Travel entry
let travelRequest = UpdateTimelogRequest(
    type: "CLOCK_IN",
    latitude: 37.7849,
    longitude: -122.4094,
    checkedTime: "2023-12-07T14:00:00Z",
    projectUid: nil,
    jobUid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    timelogType: "TRAVEL",
    reasonCode: nil,
    remarks: "Traveling to next job site",
    laborCodeUid: nil
)

let travelResult = try await zync.timelog.updateTimelog(request: travelRequest)

Timelog Types and Auto-Entry Logic

The timelog system supports multiple activity types with intelligent auto-entry logic:

  • JOB: Work time on the job site
  • TRAVEL: Travel time between locations
  • MEAL_BREAK: Break time for meals

Auto-Entry Features:

  • Travel → Job: Automatically closes active TRAVEL when starting JOB
  • Meal Break Auto-Close: Closes active MEAL_BREAK before any new entry
  • Job → Meal Break: Auto-starts MEAL_BREAK when clocking out with MEAL_BREAK reason

Error Handling

Common Error Types

when (result) {
    is GetJobTimeLogSummaryResult.Success -> {
        // Handle success
        val summary = result.data
        displayTimelogSummary(summary)
    }
    is GetJobTimeLogSummaryResult.Failure -> {
        when (result.error) {
            is ZyncError.Network -> {
                println("Network error - showing cached data")
                // Show offline indicator but continue with cached data
                showOfflineIndicator()
                loadCachedTimelogData()
            }
            is ZyncError.Error -> {
                println("API error: ${result.error.message}")
                result.error.code?.let { code ->
                    when (code) {
                        401 -> showLoginPrompt()
                        403 -> showAccessDeniedMessage()
                        404 -> showNoTimelogMessage()
                        else -> showGenericErrorMessage()
                    }
                }
            }
        }
    }
}
switch onEnum(of: result) {
case .success(let success):
    // Handle success
    let summary = success.data
    displayTimelogSummary(summary)
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()
        loadCachedTimelogData()
    case .error(let error):
        print("API error: \(error.message)")
        if let code = error.code {
            switch code {
            case 401:
                showLoginPrompt()
            case 403:
                showAccessDeniedMessage()
            case 404:
                showNoTimelogMessage()
            default:
                showGenericErrorMessage()
            }
        }
    }
}

Offline-First Behavior

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

Timelog Data Access

First Time Access:

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

Subsequent Access:

  • Returns cached timelog 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 entries

Offline Timelog Operations

// First call - performs initial sync
val initialResult = zync.timelog.getJobTimeLogSummary(
    jobUid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    isTimeLogV2Enabled = true
)
// User waits for sync to complete

// Subsequent calls - immediate response + background sync
val cachedResult = zync.timelog.getJobTimeLogSummary(
    jobUid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    isTimeLogV2Enabled = true
)
// 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.timelog.getJobTimeLogSummary(
    jobUid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    isTimeLogV2Enabled: true
)
// User waits for sync to complete

// Subsequent calls - immediate response + background sync
let cachedResult = try await zync.timelog.getJobTimeLogSummary(
    jobUid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    isTimeLogV2Enabled: true
)
// User gets immediate response with cached data
// SDK performs background delta sync for any updates since last sync

Network Resilience

Offline Operations:

  • All timelog updates work offline and queue for sync
  • Local database serves as single source of truth
  • Changes are preserved and synced when connectivity returns
  • Auto-entry logic works completely offline

Online Operations:

  • Background sync runs automatically without blocking UI
  • Smart conflict resolution for concurrent modifications
  • Automatic retry with exponential backoff for failed operations
  • Server state takes precedence during sync conflicts

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 timelog data.

Best Practices

Optimal Usage Patterns

Efficient Timelog Management

class TimelogViewModel {
    private var cachedSummary: ZyncJobTimeLogSummary? = null
    private var activeTimeLog: ZyncActiveTimeLog? = null

    suspend fun loadTimelogData(jobUid: String) {
        // Load summary data
        val summaryResult = zync.timelog.getJobTimeLogSummary(jobUid, true)
        when (summaryResult) {
            is GetJobTimeLogSummaryResult.Success -> {
                cachedSummary = summaryResult.data
                updateTimelogUI(summaryResult.data)
            }
            is GetJobTimeLogSummaryResult.Failure -> {
                handleError(summaryResult.error)
            }
        }

        // Check active timelog
        val activeResult = zync.timelog.getActiveTimeLog()
        when (activeResult) {
            is GetActiveTimeLogResult.Success -> {
                activeTimeLog = activeResult.data
                updateActiveStatusUI(activeResult.data)
            }
            is GetActiveTimeLogResult.Failure -> {
                handleError(activeResult.error)
            }
        }
    }

    suspend fun clockIn(jobUid: String, location: Pair<Double, Double>?) {
        val request = UpdateTimelogRequest(
            type = "CLOCK_IN",
            latitude = location?.first,
            longitude = location?.second,
            checkedTime = getCurrentISOTime(),
            jobUid = jobUid,
            timelogType = "JOB",
            remarks = "Started work"
        )

        val result = zync.timelog.updateTimelog(request)
        // Handle result and refresh active timelog
        refreshActiveTimelog()
    }
}

Offline-First Operations

// Update timelog offline - it'll sync automatically
suspend fun handleTimelogEntry(type: String, jobUid: String) {
    val request = UpdateTimelogRequest(
        type = type,
        latitude = getCurrentLocation()?.latitude,
        longitude = getCurrentLocation()?.longitude,
        checkedTime = getCurrentISOTime(),
        jobUid = jobUid,
        timelogType = "JOB",
        remarks = if (type == "CLOCK_IN") "Started work" else "Completed work"
    )

    val result = zync.timelog.updateTimelog(request)
    // Entry is immediately available locally
    // Will sync to server when connectivity is available

    when (result) {
        is UpdateTimelogResult.Success -> {
            showSuccessMessage("Timelog updated successfully")
            refreshTimelogData()
        }
        is UpdateTimelogResult.Failure -> {
            // Even on "failure", entry might be saved locally
            showMessage("Timelog saved locally, will sync when online")
        }
    }
}

Performance Guidelines

Do’s ✅

  • Let the SDK manage sync timing automatically
  • Use cached data for immediate UI responsiveness
  • Update timelog entries optimistically (offline-first)
  • Embrace background sync for keeping data current
  • Handle auto-entry logic gracefully in UI

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
  • Don’t assume timelog operations will fail offline

Error Handling Best Practices

suspend fun handleTimelogOperations(jobUid: String) {
    try {
        val result = zync.timelog.getJobTimeLogSummary(jobUid, true)
        when (result) {
            is GetJobTimeLogSummaryResult.Success -> {
                // Handle success
                displayTimelogSummary(result.data)
            }
            is GetJobTimeLogSummaryResult.Failure -> {
                when (result.error) {
                    is ZyncError.Network -> {
                        // Show offline indicator but continue with cached data
                        showOfflineIndicator()
                        loadCachedTimelogData()
                    }
                    is ZyncError.Error -> {
                        // Log error and show user-friendly message
                        logError(result.error)
                        showErrorMessage("Unable to sync timelog. Using cached data.")
                        loadCachedTimelogData()
                    }
                }
            }
        }
    } catch (exception: Exception) {
        // Handle unexpected errors gracefully
        logError(exception)
        showGenericErrorMessage()
    }
}

Security Considerations

  • Offline Security: Timelog entries are stored securely in encrypted local database
  • Sync Security: All network operations use secure connections
  • Location Privacy: Location data is handled according to privacy settings
  • Time Accuracy: Local timestamps are validated and synchronized with server time

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.

Time Calculation Features

The timelog system provides sophisticated time calculation capabilities:

Multi-Activity Support

  • Concurrent Activities: Handles multiple activity types simultaneously
  • Session Matching: Pairs CLOCK_IN/CLOCK_OUT events by activity type
  • Chronological Processing: Processes all entries in time order for accurate calculations

Robust Error Handling

  • Missing Clock-outs: Handles incomplete sessions gracefully
  • Invalid Timestamps: Validates and corrects timestamp inconsistencies
  • Overlap Resolution: Resolves overlapping time entries intelligently

Summary Calculation

  • Total Time Breakdown: Calculates work, break, and travel time separately
  • Session Duration: Computes individual session durations
  • Real-time Updates: Updates calculations as new entries are added

Note: Time calculations are performed locally for immediate responsiveness and validated during server synchronization.


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