TimeTrackingService
Purpose
High-level abstraction over FileStorageService providing business-logic operations for time tracking. Simplifies common workflows like starting/stopping timers and managing tags.
Protocol
public protocol TimeTrackingServiceProtocol: Sendable {
func getAllTags() async throws -> [Tag]
func createTag(name: String, color: String) async throws -> Tag
func updateTag(_ tag: Tag) async throws
func deleteTag(_ tag: Tag) async throws
func archiveTag(_ tag: Tag) async throws
func unarchiveTag(_ tag: Tag) async throws
func startTimer(tagId: UUID?, comment: String?) async throws -> TimeRecord
func stopTimer(_ record: TimeRecord) async throws -> TimeRecord
func getRunningTimers() async throws -> [TimeRecord]
func getRecords(from startDate: Date, to endDate: Date) async throws -> [TimeRecord]
func getTodayRecords() async throws -> [TimeRecord]
func updateRecord(_ record: TimeRecord) async throws
func deleteRecord(_ record: TimeRecord) async throws
func createManualRecord(startTime: Date, endTime: Date?, tagId: UUID?, comment: String?) async throws -> TimeRecord
// Image operations
func addImage(_ data: Data, to record: TimeRecord, ext: String) async throws -> TimeRecord
func removeImage(at index: Int, from record: TimeRecord) async throws -> TimeRecord
} Implementation
public actor TimeTrackingService: TimeTrackingServiceProtocol {
private let storage: FileStorageService
public init(storage: FileStorageService)
} Uses Swift actor model for thread-safe concurrent access.
Tag Operations
| Method | Description |
|---|---|
getAllTags() | Retrieve all tags from storage |
createTag(name:color:) | Create new tag with name and hex color |
updateTag(_:) | Update existing tag properties |
deleteTag(_:) | Remove tag from storage |
archiveTag(_:) | Archive tag (hidden from selection UI) |
unarchiveTag(_:) | Unarchive tag (restore to active) |
Timer Operations
| Method | Description |
|---|---|
startTimer(tagId:comment:) | Start new timer, returns TimeRecord with nil endTime |
stopTimer(_:) | Stop running timer, sets endTime to current time |
getRunningTimers() | Get all records with nil endTime |
Record Operations
| Method | Description |
|---|---|
getRecords(from:to:) | Query records within date range |
getTodayRecords() | Convenience method for today’s records |
updateRecord(_:) | Update existing record. Refused with AutomergeStorageError.recordMayBeInUnreadableMonth when no readable month holds the id while a month file is unreadable |
restoreRecord(_:) | Write back a record that storage does not hold; see below |
deleteRecord(_:) | Remove record from storage |
createManualRecord(startTime:endTime:tagId:comment:) | Create record with explicit times |
restoreRecord
restoreRecord(_:) is an actor method, not part of TimeTrackingServiceProtocol. It validates the time range and stores the caller’s value as it is, id included, through storage.saveRecord. The app uses it for the undo of a delete (AppState.undoDelete) and for the records built by the in-app HEY import.
Use it instead of updateRecord whenever the record is not expected to be in storage. updateRecord edits an existing record and is refused while a month file is unreadable and no readable month holds the id; a restore has to work in that state too. Never use it to edit a record that is in storage: it does not look for the record in other months, so a changed start time would leave a duplicate behind. Storage side: AutomergeStorageService.
Image Operations
| Method | Description |
|---|---|
addImage(_:to:ext:) | Add image to record, returns updated record with new filename |
removeImage(at:from:) | Remove image at index from record |
Images are stored via FileStorageService and referenced by filename in TimeRecord.images array.
Both keep the record and the files consistent when storage refuses the record update (for example recordMayBeInUnreadableMonth):
addImagewrites the file, then updates the record. If the update throws, the file just written is removed again and the error is rethrown, so no orphan is left.removeImageupdates the record first and deletes the file after. If the update throws, the file is untouched and the record still lists it. A file that cannot be deleted after a successful update is logged and left as an orphan (reported byminuta doctor); the call still returns the updated record.
Related
Models
- 201-tag - Tag data structure
- 202-time-record - TimeRecord with images
Services
- 301-file-storage - Underlying storage layer
- 302-export - Uses records for CSV/report export
UI
- 403-app-state - Wraps this service for UI
- 406-record-editor - Timer start/stop, record edits
- 405-settings-sheet - Tag management
Testing
- MockTimeTrackingService for unit tests