286 lines
8.0 KiB
Swift
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)
|
|
}
|
|
}
|