Files
bulkhealth/BulkHealthApp/FieldMappingViews.swift
T

286 lines
8.0 KiB
Swift

import SwiftUI
struct FieldMappingListView: View {
@ObservedObject var viewModel: ImportFlowViewModel
var body: some View {
List {
ForEach(viewModel.template.fields) { field in
NavigationLink {
FieldMappingDetailView(viewModel: viewModel, field: field)
} label: {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(field.title)
.font(.headline)
Text(field.targetFormat)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Text(viewModel.fieldStateText(for: field))
.font(.caption2)
.foregroundStyle(color(for: viewModel.fieldState(for: field)))
}
}
}
}
.navigationTitle(String(localized: "screen.title.field_mapping"))
}
private func color(for state: ImportFlowViewModel.FieldMappingState) -> Color {
switch state {
case .incomplete: return .secondary
case .partiallyValid: return .yellow
case .valid: return .green
}
}
}
struct FieldMappingDetailView: View {
@ObservedObject var viewModel: ImportFlowViewModel
let field: TemplateField
var body: some View {
SwiftUI.Form {
targetSection
sourceSection
formatterSection
}
.navigationTitle(field.title)
.navigationBarTitleDisplayMode(.inline)
}
private var targetSection: some View {
Section(
header: Text(String(localized: "section.target")),
footer: Text(field.id == "date" ? String(localized: "field.date.helper") : "")
) {
Text(
field.targetExplanation + " " + String.localizedStringWithFormat(
NSLocalizedString("label.example_value", comment: ""),
field.targetExample
)
)
if let allowed = field.targetAllowedValues, !allowed.isEmpty {
Text(allowed)
.font(.caption2)
.foregroundStyle(.secondary)
}
}
}
private var sourceSection: some View {
Section(header: Text(String(localized: "section.mapping"))) {
if viewModel.sourceKeys.isEmpty {
Text(String(localized: "text.mapping.unavailable"))
.foregroundStyle(.secondary)
} else {
ForEach(viewModel.sourceKeys, id: \.self) { sourceKey in
Button {
viewModel.mapping.sourceKeyByFieldID[field.id] = sourceKey
} label: {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(sourceKey)
Spacer()
if viewModel.mappedSourceKey(for: field.id) == sourceKey {
Image(systemName: "checkmark")
.foregroundStyle(.green)
}
}
Text(viewModel.sourceExample(for: sourceKey))
.font(.caption2)
.foregroundStyle(.secondary)
.lineLimit(1)
}
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
}
}
}
private var formatterSection: some View {
Section(header: Text(String(localized: "section.formatting"))) {
NavigationLink(String(localized: "button.select_formatter")) {
FormatterSelectionView(viewModel: viewModel, field: field)
}
Text(String.localizedStringWithFormat(
NSLocalizedString("label.formatter_coverage", comment: ""),
viewModel.formattingPreview(for: field).matched,
viewModel.formattingPreview(for: field).total
))
.foregroundStyle(viewModel.formattingPreview(for: field).isPartial ? .yellow : .secondary)
}
}
}
struct FormatterSelectionView: View {
@ObservedObject var viewModel: ImportFlowViewModel
let field: TemplateField
var body: some View {
Form {
Section(header: Text(String(localized: "section.formatting"))) {
ForEach(viewModel.formatterOptions(for: field)) { formatterType in
Button {
viewModel.updateFormatter(for: field.id) { $0.formatterType = formatterType }
} label: {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(viewModel.formatterDisplayName(formatterType))
Text(String.localizedStringWithFormat(
NSLocalizedString("label.formatter_matches", comment: ""),
viewModel.formatterMatchSummary(for: field, formatterType: formatterType).matched,
viewModel.formatterMatchSummary(for: field, formatterType: formatterType).total
))
.font(.caption2)
.foregroundStyle(formatterMatchColor(for: formatterType))
}
Spacer()
if viewModel.formatter(for: field.id).formatterType == formatterType {
Image(systemName: "checkmark")
.foregroundStyle(.green)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
}
Section(header: Text(String(localized: "section.test_area"))) {
Text(String.localizedStringWithFormat(
NSLocalizedString("label.formatter_matches", comment: ""),
previewMatchedCount,
previewRows.count
))
.font(.caption)
.foregroundStyle(previewCoverageColor)
if previewRows.isEmpty {
Text(String(localized: "text.no_source_values"))
.foregroundStyle(.secondary)
} else {
ForEach(Array(previewRows.enumerated()), id: \.offset) { _, row in
VStack(alignment: .leading, spacing: 4) {
Text(row.input)
.font(.caption)
Text(row.output.isEmpty ? String(localized: "text.formatter_empty") : row.output)
.font(.caption2)
.foregroundStyle(row.valid ? .green : .red)
}
}
}
}
}
.navigationTitle(String(localized: "screen.title.formatter"))
.navigationBarTitleDisplayMode(.inline)
}
private var previewRows: [(input: String, output: String, valid: Bool)] {
viewModel.previewRows(for: field, limit: 5)
}
private var previewMatchedCount: Int {
previewRows.filter { $0.valid }.count
}
private var previewCoverageColor: Color {
if previewRows.isEmpty {
return .secondary
}
return previewMatchedCount == previewRows.count ? .green : .yellow
}
private func formatterMatchColor(for formatterType: FieldFormatterType) -> Color {
let summary = viewModel.formatterMatchSummary(for: field, formatterType: formatterType)
return summary.matched == summary.total && summary.total > 0 ? .green : .secondary
}
}
struct ImportPlanView: View {
@ObservedObject var viewModel: ImportFlowViewModel
var body: some View {
SwiftUI.Form {
Section(String(localized: "section.plan_summary")) {
let summary = viewModel.mappingSummary()
Text(String.localizedStringWithFormat(
NSLocalizedString("label.fields_ready", comment: ""),
summary.validFields,
summary.totalFields
))
Text(String.localizedStringWithFormat(
NSLocalizedString("label.rows_ready", comment: ""),
summary.validRows,
summary.totalRows
))
}
Section(String(localized: "section.template")) {
ForEach(viewModel.template.fields) { field in
VStack(alignment: .leading, spacing: 4) {
Text(field.title)
.font(.headline)
Text(String.localizedStringWithFormat(
NSLocalizedString("label.source_for_field", comment: ""),
viewModel.mappedSourceKey(for: field.id)
))
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
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)
}
}
.navigationTitle(String(localized: "screen.title.import_plan"))
.navigationBarTitleDisplayMode(.inline)
}
}