Files
bulkhealth/BulkHealthApp/ContentView.swift
2026-03-01 11:43:38 +01:00

201 lines
5.0 KiB
Swift

import SwiftUI
struct ContentView: View {
@ObservedObject var viewModel: ImportFlowViewModel
var body: some View {
NavigationStack {
List {
NavigationLink {
SegmentImportView(viewModel: viewModel)
} label: {
VStack(alignment: .leading, spacing: 4) {
Text(viewModel.template.localizedHealthTypeName)
.font(.headline)
Text(String(localized: "label.template"))
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
.navigationTitle(String(localized: "screen.title.segments"))
}
}
}
private struct SegmentImportView: View {
@ObservedObject var viewModel: ImportFlowViewModel
@State private var showingFileImporter = false
@State private var fieldStates: [String: ImportFlowViewModel.FieldMappingState] = [:]
var body: some View {
Form {
sourceSection
fieldsSection
testImportSection
}
.navigationTitle(viewModel.template.localizedHealthTypeName)
.navigationBarTitleDisplayMode(.inline)
.fileImporter(
isPresented: $showingFileImporter,
allowedContentTypes: viewModel.supportedContentTypes,
allowsMultipleSelection: false
) { result in
switch result {
case let .success(urls):
if let url = urls.first {
viewModel.loadFile(from: url)
}
case let .failure(error):
viewModel.errorMessage = error.localizedDescription
}
}
.alert(String(localized: "error.title"), isPresented: Binding(
get: { viewModel.errorMessage != nil },
set: { isVisible in
if !isVisible {
viewModel.errorMessage = nil
}
}
), actions: {
Button(String(localized: "button.ok"), role: .cancel) {}
}, message: {
Text(viewModel.errorMessage ?? "")
})
.onAppear {
refreshFieldStates()
}
.onChange(of: viewModel.entries.count) { _, _ in
refreshFieldStates()
}
.onChange(of: viewModel.mapping) { _, _ in
refreshFieldStates()
}
.onChange(of: viewModel.fieldFormatters) { _, _ in
refreshFieldStates()
}
.onChange(of: viewModel.moodValenceScale) { _, _ in
refreshFieldStates()
}
}
private var sourceSection: some View {
Section(String(localized: "section.source")) {
Button(String(localized: "button.load_json")) {
showingFileImporter = true
}
Button(String(localized: "button.load_sample")) {
viewModel.loadBundledSample()
}
Text(String.localizedStringWithFormat(
NSLocalizedString("label.records_loaded", comment: ""),
viewModel.entries.count
))
if !viewModel.statusMessage.isEmpty {
Text(viewModel.statusMessage)
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
private var fieldsSection: some View {
Section(String(localized: "section.mapping")) {
ForEach(viewModel.template.fields) { field in
NavigationLink {
FieldMappingDetailView(viewModel: viewModel, field: field)
} label: {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(field.title)
Text(field.targetFormat)
.font(.caption2)
.foregroundStyle(.secondary)
}
Spacer()
Text(fieldStateText(fieldStates[field.id] ?? .incomplete))
.font(.caption2)
.foregroundStyle(color(for: fieldStates[field.id] ?? .incomplete))
}
}
}
}
}
private var testImportSection: some View {
Section(String(localized: "section.commit")) {
Button(String(localized: "button.run_dry_run")) {
viewModel.runDryRun()
}
if let result = viewModel.dryRunResult {
Text(String.localizedStringWithFormat(
NSLocalizedString("label.preview_count", comment: ""),
result.drafts.count
))
if !result.errors.isEmpty {
Text(String.localizedStringWithFormat(
NSLocalizedString("label.errors_count", comment: ""),
result.errors.count
))
.foregroundStyle(.red)
}
if !result.warnings.isEmpty {
Text(String.localizedStringWithFormat(
NSLocalizedString("label.warnings_count", comment: ""),
result.warnings.count
))
.foregroundStyle(.orange)
}
}
Button {
Task {
await viewModel.commitImport()
}
} label: {
if viewModel.isImporting {
ProgressView()
} else {
Text(String(localized: "button.commit"))
}
}
.disabled(viewModel.isImporting)
Text(String(localized: "text.commit_notice"))
.font(.caption)
.foregroundStyle(.secondary)
}
}
private func color(for state: ImportFlowViewModel.FieldMappingState) -> Color {
switch state {
case .incomplete:
return .secondary
case .partiallyValid:
return .yellow
case .valid:
return .green
}
}
private func fieldStateText(_ state: ImportFlowViewModel.FieldMappingState) -> String {
switch state {
case .incomplete:
return String(localized: "state.incomplete")
case .partiallyValid:
return String(localized: "state.partial")
case .valid:
return String(localized: "state.valid")
}
}
private func refreshFieldStates() {
var next: [String: ImportFlowViewModel.FieldMappingState] = [:]
for field in viewModel.template.fields {
next[field.id] = viewModel.fieldState(for: field)
}
fieldStates = next
}
}