Export and Import Services

ExportService

Protocol

protocol ExportServiceProtocol {
    func exportToCSV(
        records: [TimeRecord],
        tags: [Tag],
        options: ExportOptions
    ) -> Data
}

CSV Format

Date,Start Time,End Time,Duration,Tag,Comment
2024-01-15,09:30,11:45,2:15,Work,Working on feature X
  • Columns follow the declaration order of ExportOptions.Column (Date, Start Time, End Time, Duration, Tag, Comment), restricted to options.includeColumns. The set’s own order is not used, so a subset keeps the same relative order (e.g. Date,Duration,Tag).
  • Every value is passed through CSVParser.escapeField, tag names included: a value containing a comma, a double quote or a line break is wrapped in double quotes, with inner quotes doubled.
  • A running record has ongoing in the End Time column.
  • Rows are sorted by start time, oldest first.
  • Two tags with the same id (possible in synced storage) do not trap the export: the first one wins.

Toggl layout

ExportOptions.layout = .toggl (default .minuta) writes Toggl Track’s time entries CSV instead, as Toggl’s own export does: UTF-8 BOM, every field quoted.

"User","Email","Client","Project","Task","Description","Billable","Start date","Start time","End date","End time","Duration","Tags"
"","","","avito-pii","","Docs","No","2026-01-12","09:00:00","2026-01-12","10:30:00","01:30:00","docs"
  • Project is the record’s current tag; Tags, Client, Task, Billable come from the record’s toggl.* metadata (kept by the Toggl import), otherwise empty and No; User and Email are empty.
  • Dates yyyy-MM-dd, times and Duration HH:mm:ss, local time zone. includeColumns and the date/time formats do not apply.
  • Running records are left out (Toggl has no open entries).
  • Toggl’s importer computes the end from start and duration; it may need Email filled in to assign the entries to a workspace member.

Export Options

  • Date range filtering
  • Tag filtering
  • Include/exclude untagged
  • Include/exclude running timers
  • Column selection

Implementation

DefaultExportService in Shared/Sources/MinutaShared/Services/ExportService.swift

ImportService

Protocol

protocol ImportServiceProtocol: Sendable {
    func parse(_ content: String, existingTags: [Tag]) -> ImportParseResult
}

struct ImportParseResult: Sendable {
    let records: [ParsedRecord]
    let newTags: [Tag]          // needed tags missing from existingTags, case-insensitive
    let skippedRows: Int        // unreadable time, end before start, too few fields
    let zeroDurationRows: Int   // end == start, imported one second long
}

HEY CSV Format

Start,End,Duration,Category,Notes
2024-01-15 09:30,2024-01-15 11:45,2:15,Work,Working on feature X
  • Dates: YYYY-MM-DD HH:mm
  • Duration is ignored (calculated from start/end)
  • Category maps to tag name
  • Notes maps to comment

ParsedRecord

struct ParsedRecord: Sendable {
    let startTime: Date
    let endTime: Date
    let categoryName: String?
    let notes: String?
    let metadata: [String: String]  // TimeRecord.metadata, empty for HEY
}

Import Flow

  1. Parse CSV to get ParsedRecord array and new tags
  2. Create new tags (auto-assigned random colors from ColorPalette)
  3. Build tag name -> UUID lookup
  4. Create TimeRecord for each ParsedRecord with resolved tagId
  5. Save records via TimeTrackingService

Toggl CSV Format

TogglImportService: Toggl Track’s time entries export (User,Email,Client,Project,Task,Description,Billable,Start date,Start time,End date,End time,Duration,Tags, BOM, every field quoted).

  • Columns are found by header name; Start date, Start time, End date, End time are required (isTogglHeader), otherwise nothing is parsed
  • Start date + Start time (yyyy-MM-dd HH:mm:ss, DateFormatters.togglCSV) read in the local time zone; Duration ignored
  • Project maps to tag name (empty = untagged), Description to comment
  • Every other non-empty column except User and Email goes to metadata as toggl.<column lowercased>, plus source = toggl
  • End equal to start: imported one second long (zeroDurationRows); end before start or an unreadable time: skipped (skippedRows)

Implementation

HEYImportService and TogglImportService in Shared/Sources/MinutaShared/Services/ImportService.swift

Uses CSVParser utility for proper field parsing (handles quoted fields, embedded commas).

UI Integration

Export

  • HistorySection uses fileExporter modifier for save panel
  • ShareLink for system share sheet
  • CSV comes from appState.exportService.exportToCSV (the same DefaultExportService the CLI uses)

Import

  • SettingsSheet uses fileImporter modifier (“Import from HEY”, “Import from Toggl”)
  • Processes the file with the chosen format’s service
  • Shows success/error message

Models

Services

UI