201 lines
5.0 KiB
Swift
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
|
|
}
|
|
}
|