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 tooptions.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
ongoingin theEnd Timecolumn. - 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" Projectis the record’s current tag;Tags,Client,Task,Billablecome from the record’stoggl.*metadata (kept by the Toggl import), otherwise empty andNo;UserandEmailare empty.- Dates
yyyy-MM-dd, times and DurationHH:mm:ss, local time zone.includeColumnsand 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
Emailfilled 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
- Parse CSV to get
ParsedRecordarray and new tags - Create new tags (auto-assigned random colors from ColorPalette)
- Build tag name -> UUID lookup
- Create TimeRecord for each ParsedRecord with resolved tagId
- 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 timeare required (isTogglHeader), otherwise nothing is parsed Start date+Start time(yyyy-MM-dd HH:mm:ss,DateFormatters.togglCSV) read in the local time zone;DurationignoredProjectmaps to tag name (empty = untagged),Descriptionto comment- Every other non-empty column except
UserandEmailgoes tometadataastoggl.<column lowercased>, plussource = 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
fileExportermodifier for save panel - ShareLink for system share sheet
- CSV comes from
appState.exportService.exportToCSV(the sameDefaultExportServicethe CLI uses)
Import
- SettingsSheet uses
fileImportermodifier (“Import from HEY”, “Import from Toggl”) - Processes the file with the chosen format’s service
- Shows success/error message
Related
Models
- 202-time-record - Source data for export
- 201-tag - Tag names and colors
- 203-export-options - Export configuration
Services
- 301-file-storage - Data retrieval
- 303-time-tracking - Record saving after import
- 306-report - Visual report generation (PDF/SVG/PNG)
UI
- 411-share-export-sheet - Export format selection
- 407-history-section - Export button location
- 405-settings-sheet - Import from HEY