feat: Ipad app production readiness, Colony orchestration, Social posting (#44)
All checks were successful
Production Readiness / backend-contracts (push) Successful in 1m47s
Production Readiness / webos-typecheck (push) Successful in 1m50s
Production Readiness / ipad-parse (push) Successful in 1m34s

#38 Ipad app production readiness, Colony orchestration, Social posting

Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local>
Reviewed-on: #44
This commit was merged in pull request #44.
This commit is contained in:
2026-05-03 18:30:38 +05:30
parent 59d398abc3
commit eeb684b46c
86 changed files with 20349 additions and 1655 deletions

View File

@@ -1,16 +1,39 @@
import Combine
import SwiftUI
private struct ImportRemediationDraft: Identifiable {
let batchId: String
let proposal: VelocityImportProposalDTO
let workbenchRow: VelocityImportWorkbenchRowDTO?
let fields: [String]
var id: String { proposal.proposalId }
init(batchId: String, proposal: VelocityImportProposalDTO, workbenchRow: VelocityImportWorkbenchRowDTO?) {
self.batchId = batchId
self.proposal = proposal
self.workbenchRow = workbenchRow
let canonicalFields: [String] = proposal.payload?.canonicalPayload.map { Array($0.keys) } ?? []
let missingFields = proposal.payload?.missingRequired ?? []
let unresolvedFields = proposal.payload?.unresolvedFields ?? []
let diffFields = workbenchRow?.fieldDiffs.map(\.field) ?? []
let validationFields = workbenchRow?.validation.map(\.field) ?? []
let combinedFields: [String] = canonicalFields + missingFields + unresolvedFields + diffFields + validationFields
fields = Array(Set<String>(combinedFields)).sorted()
}
}
struct ImportsView: View {
@State private var appStore = AppStore.shared
@State private var batches: [VelocityImportBatchSummaryDTO] = []
@State private var selectedBatch: VelocityImportBatchSummaryDTO?
@State private var detail: VelocityImportBatchDetailDTO?
@State private var workbench: VelocityImportWorkbenchDTO?
@State private var isLoading = false
@State private var isCommitting = false
@State private var activeProposalID: String?
@State private var errorMessage: String?
@State private var successMessage: String?
private let refreshTimer = Timer.publish(every: 30, on: .main, in: .common).autoconnect()
@State private var remediationDraft: ImportRemediationDraft?
var body: some View {
HStack(spacing: 0) {
@@ -24,20 +47,40 @@ struct ImportsView: View {
detailPane
}
.background(VelocityTheme.background)
.task { await loadBatches(selectFirst: true) }
.task {
await appStore.ensureCRMVocabulariesLoaded()
await loadBatches(selectFirst: true)
}
.refreshable { await loadBatches(selectFirst: false) }
.onReceive(refreshTimer) { _ in
Task { await loadBatches(selectFirst: false, silent: true) }
.sheet(item: $remediationDraft) { draft in
ImportRemediationSheet(
draft: draft,
duplicatePolicies: appStore.crmVocabularies.importDuplicatePolicies
) { decision, notes, fieldOverrides, duplicatePolicy in
Task {
await reviewProposal(
batchId: draft.batchId,
proposal: draft.proposal,
decision: decision,
notes: notes,
fieldOverrides: fieldOverrides,
duplicatePolicy: duplicatePolicy
)
}
}
}
}
private var batchRail: some View {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 4) {
Text("Imports")
.font(.system(size: 26, weight: .bold))
.foregroundStyle(VelocityTheme.foreground)
Text("Canonical CRM import review and commit queue.")
HStack {
Text("Imports")
.font(.system(size: 26, weight: .bold))
.foregroundStyle(VelocityTheme.foreground)
Spacer()
}
Text("Read-only canonical CRM import review and remediation queue.")
.font(.system(size: 12))
.foregroundStyle(VelocityTheme.mutedFg)
}
@@ -77,6 +120,9 @@ struct ImportsView: View {
VStack(alignment: .leading, spacing: 18) {
if let detail {
detailHeader(detail)
if let workbench {
workbenchPanel(workbench)
}
proposalsPanel(detail)
} else if isLoading {
loadingCard("Loading import detail...")
@@ -195,8 +241,115 @@ struct ImportsView: View {
.glassCard(cornerRadius: 20)
}
private func proposalCard(_ proposal: VelocityImportProposalDTO, batchId: String) -> some View {
private func workbenchPanel(_ workbench: VelocityImportWorkbenchDTO) -> some View {
VStack(alignment: .leading, spacing: 14) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("Remediation Workbench")
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(VelocityTheme.foreground)
Text("Validation, duplicate detection, and canonical CRM row diffs before commit.")
.font(.system(size: 12))
.foregroundStyle(VelocityTheme.mutedFg)
}
Spacer()
Button {
Task {
if let batchId = detail?.batchId {
await refreshWorkbench(batchId: batchId)
}
}
} label: {
Image(systemName: "arrow.clockwise")
}
.buttonStyle(.plain)
.foregroundStyle(VelocityTheme.accent)
}
HStack(spacing: 12) {
metricCard("Rows", value: "\(workbench.summary.proposalCount)", color: VelocityTheme.accent)
metricCard("Duplicates", value: "\(workbench.summary.duplicateCount)", color: VelocityTheme.warning)
metricCard("Errors", value: "\(workbench.summary.validationErrorCount)", color: VelocityTheme.danger)
metricCard("Warnings", value: "\(workbench.summary.validationWarningCount)", color: VelocityTheme.warning)
}
LazyVStack(spacing: 10) {
ForEach(workbench.rows.prefix(20)) { row in
workbenchRowCard(row)
}
}
}
.padding(18)
.glassCard(cornerRadius: 20)
}
private func workbenchRowCard(_ row: VelocityImportWorkbenchRowDTO) -> some View {
VStack(alignment: .leading, spacing: 10) {
HStack {
Text(row.rowNumber.map { "Row \($0)" } ?? "Proposal \(row.proposalId.prefix(8))")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(VelocityTheme.foreground)
Spacer()
Text("\(confidencePercent(row.confidence))%")
.font(.system(size: 11, weight: .bold))
.foregroundStyle(VelocityTheme.accent)
Text(row.status.replacingOccurrences(of: "_", with: " ").capitalized)
.font(.system(size: 10, weight: .semibold))
.foregroundStyle(lifecycleColor(row.status))
}
if !row.validation.isEmpty {
VStack(alignment: .leading, spacing: 5) {
ForEach(row.validation) { issue in
HStack(alignment: .top, spacing: 6) {
Image(systemName: issue.severity == "error" ? "xmark.octagon" : "exclamationmark.triangle")
.font(.system(size: 11, weight: .semibold))
.foregroundStyle(issue.severity == "error" ? VelocityTheme.danger : VelocityTheme.warning)
Text("\(issue.field): \(issue.message)")
.font(.system(size: 11))
.foregroundStyle(VelocityTheme.mutedFg)
}
}
}
}
if let duplicate = row.duplicateCandidates.first {
Text("Duplicate candidate: \(duplicate.fullName) · \(duplicate.matchReason) match · \(duplicate.matchScore)%")
.font(.system(size: 11, weight: .semibold))
.foregroundStyle(VelocityTheme.warning)
}
let changedDiffs = row.fieldDiffs.filter(\.changed)
if !changedDiffs.isEmpty {
VStack(alignment: .leading, spacing: 4) {
Text("Changed fields")
.font(.system(size: 10, weight: .semibold))
.tracking(1)
.foregroundStyle(VelocityTheme.mutedFg)
ForEach(changedDiffs.prefix(4)) { diff in
Text("\(diff.field): \(diff.existing ?? "-")\(diff.proposed ?? "-")")
.font(.system(size: 11))
.foregroundStyle(VelocityTheme.foreground)
.lineLimit(1)
}
}
}
}
.padding(14)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(VelocityTheme.surface)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(row.validation.contains { $0.severity == "error" } ? VelocityTheme.danger.opacity(0.25) : VelocityTheme.borderSubtle, lineWidth: 1)
)
)
}
private func proposalCard(_ proposal: VelocityImportProposalDTO, batchId: String) -> some View {
let rowDiagnostics = workbench?.row(for: proposal.proposalId)
return VStack(alignment: .leading, spacing: 10) {
HStack {
VStack(alignment: .leading, spacing: 3) {
Text(proposal.rowLabel)
@@ -227,6 +380,16 @@ struct ImportsView: View {
.font(.system(size: 11))
.foregroundStyle(VelocityTheme.danger)
}
if let unresolved = proposal.payload?.unresolvedFields, !unresolved.isEmpty {
Text("Needs review: \(unresolved.joined(separator: ", "))")
.font(.system(size: 11))
.foregroundStyle(VelocityTheme.warning)
}
if let rowDiagnostics {
proposalDiagnostics(rowDiagnostics)
}
}
.padding(14)
.background(
@@ -239,20 +402,49 @@ struct ImportsView: View {
)
}
private func proposalDiagnostics(_ row: VelocityImportWorkbenchRowDTO) -> some View {
VStack(alignment: .leading, spacing: 5) {
if !row.validation.isEmpty {
Text("Validation: \(row.validation.map { "\($0.field) \($0.severity)" }.joined(separator: ", "))")
.font(.system(size: 11))
.foregroundStyle(row.validation.contains { $0.severity == "error" } ? VelocityTheme.danger : VelocityTheme.warning)
}
if let duplicate = row.duplicateCandidates.first {
Text("Possible duplicate: \(duplicate.fullName) (\(duplicate.matchReason), \(duplicate.matchScore)%)")
.font(.system(size: 11))
.foregroundStyle(VelocityTheme.warning)
}
}
}
private func proposalActions(_ proposal: VelocityImportProposalDTO, batchId: String) -> some View {
HStack(spacing: 8) {
Button("Approve") {
Task { await reviewProposal(batchId: batchId, proposal: proposal, decision: "approved") }
Task {
await reviewProposal(
batchId: batchId,
proposal: proposal,
decision: "approved",
duplicatePolicy: defaultDuplicatePolicyValue(for: proposal)
)
}
}
.font(.system(size: 11, weight: .semibold))
.foregroundStyle(VelocityTheme.success)
.padding(.horizontal, 10)
.padding(.vertical, 8)
.contentShape(Rectangle())
.disabled(proposal.status.lowercased() == "approved")
.disabled(proposal.status.lowercased() == "approved" || defaultDuplicatePolicyValue(for: proposal) == nil)
Button("Reject") {
Task { await reviewProposal(batchId: batchId, proposal: proposal, decision: "rejected") }
Task {
await reviewProposal(
batchId: batchId,
proposal: proposal,
decision: "rejected",
duplicatePolicy: defaultDuplicatePolicyValue(for: proposal)
)
}
}
.font(.system(size: 11, weight: .semibold))
.foregroundStyle(VelocityTheme.danger)
@@ -260,9 +452,46 @@ struct ImportsView: View {
.padding(.vertical, 8)
.contentShape(Rectangle())
.disabled(proposal.status.lowercased() == "rejected")
Button("Needs Info") {
Task {
await reviewProposal(
batchId: batchId,
proposal: proposal,
decision: "needs_more_info",
duplicatePolicy: defaultDuplicatePolicyValue(for: proposal)
)
}
}
.font(.system(size: 11, weight: .semibold))
.foregroundStyle(VelocityTheme.warning)
.padding(.horizontal, 10)
.padding(.vertical, 8)
.contentShape(Rectangle())
Button("Remediate") {
remediationDraft = ImportRemediationDraft(
batchId: batchId,
proposal: proposal,
workbenchRow: workbench?.row(for: proposal.proposalId)
)
}
.font(.system(size: 11, weight: .semibold))
.foregroundStyle(VelocityTheme.accent)
.padding(.horizontal, 10)
.padding(.vertical, 8)
.contentShape(Rectangle())
}
}
private func defaultDuplicatePolicyValue(for proposal: VelocityImportProposalDTO) -> String? {
if let policy = workbench?.rows.first(where: { $0.proposalId == proposal.proposalId })?.duplicatePolicy,
appStore.crmVocabularies.importDuplicatePolicies.contains(where: { $0.value == policy }) {
return policy
}
return appStore.crmVocabularies.importDuplicatePolicies.first?.value
}
private func loadBatches(selectFirst: Bool, silent: Bool = false) async {
if !silent {
isLoading = true
@@ -291,6 +520,7 @@ struct ImportsView: View {
await MainActor.run {
selectedBatch = batch
detail = nil
workbench = nil
errorMessage = nil
successMessage = nil
isLoading = true
@@ -303,9 +533,13 @@ struct ImportsView: View {
await MainActor.run { isLoading = true }
}
do {
let fetched = try await VelocityAPIClient.shared.fetchImportBatch(batchId: batchId)
async let detailTask = VelocityAPIClient.shared.fetchImportBatch(batchId: batchId)
async let workbenchTask = VelocityAPIClient.shared.fetchImportWorkbench(batchId: batchId)
let fetched = try await detailTask
let fetchedWorkbench = try? await workbenchTask
await MainActor.run {
detail = fetched
workbench = fetchedWorkbench
errorMessage = nil
isLoading = false
}
@@ -317,11 +551,34 @@ struct ImportsView: View {
}
}
private func refreshWorkbench(batchId: String) async {
do {
let fetchedWorkbench = try await VelocityAPIClient.shared.fetchImportWorkbench(batchId: batchId)
await MainActor.run {
workbench = fetchedWorkbench
errorMessage = nil
}
} catch {
await MainActor.run {
errorMessage = error.localizedDescription
}
}
}
private func reviewProposal(
batchId: String,
proposal: VelocityImportProposalDTO,
decision: String
decision: String,
notes: String = "Reviewed from iPad Imports workspace.",
fieldOverrides: [String: String] = [:],
duplicatePolicy: String?
) async {
guard let duplicatePolicy else {
await MainActor.run {
errorMessage = "Unable to review import row because backend duplicate policy vocabulary is unavailable."
}
return
}
await MainActor.run {
activeProposalID = proposal.proposalId
errorMessage = nil
@@ -332,12 +589,15 @@ struct ImportsView: View {
batchId: batchId,
proposalId: proposal.proposalId,
decision: decision,
notes: "Reviewed from iPad Imports workspace."
notes: notes,
fieldOverrides: fieldOverrides,
duplicatePolicy: duplicatePolicy
)
await refreshDetail(batchId: batchId, silent: true)
await MainActor.run {
activeProposalID = nil
successMessage = "Proposal \(decision)."
remediationDraft = nil
successMessage = "Proposal \(decision.replacingOccurrences(of: "_", with: " "))."
}
} catch {
await MainActor.run {
@@ -379,6 +639,11 @@ struct ImportsView: View {
detail.proposals.filter { $0.status == "approved" }.count
}
private func confidencePercent(_ value: Double) -> Int {
let normalized = value <= 1 ? value * 100 : value
return max(0, min(100, Int(normalized.rounded())))
}
private func canonicalPreview(_ payload: [String: JSONValue]) -> String {
payload
.sorted(by: { $0.key < $1.key })
@@ -462,6 +727,239 @@ struct ImportsView: View {
}
}
private struct ImportRemediationSheet: View {
let draft: ImportRemediationDraft
let duplicatePolicies: [VelocityVocabularyOptionDTO]
let onSubmit: (String, String, [String: String], String) -> Void
@Environment(\.dismiss) private var dismiss
@State private var notes: String
@State private var fieldOverrides: [String: String]
@State private var duplicatePolicy: String
init(
draft: ImportRemediationDraft,
duplicatePolicies: [VelocityVocabularyOptionDTO],
onSubmit: @escaping (String, String, [String: String], String) -> Void
) {
self.draft = draft
self.duplicatePolicies = duplicatePolicies
self.onSubmit = onSubmit
_notes = State(initialValue: "")
let canonicalPayload = draft.proposal.payload?.canonicalPayload ?? [:]
let initialOverrides: [String: String] = Dictionary<String, String>(
uniqueKeysWithValues: draft.fields.map { field in
(field, canonicalPayload[field]?.stringValue ?? "")
}
)
_fieldOverrides = State(initialValue: initialOverrides)
let initialPolicy = duplicatePolicies.first(where: { $0.value == draft.workbenchRow?.duplicatePolicy })?.value
?? duplicatePolicies.first?.value
?? ""
_duplicatePolicy = State(initialValue: initialPolicy)
}
var body: some View {
NavigationStack {
ScrollView {
VStack(alignment: .leading, spacing: 18) {
VStack(alignment: .leading, spacing: 6) {
Text(draft.proposal.rowLabel)
.font(.system(size: 24, weight: .bold))
.foregroundStyle(VelocityTheme.foreground)
Text("\(draft.proposal.confidencePercent)% confidence · \(draft.proposal.status.replacingOccurrences(of: "_", with: " ").capitalized)")
.font(.system(size: 13))
.foregroundStyle(VelocityTheme.mutedFg)
}
if let workbenchRow = draft.workbenchRow {
remediationDiagnostics(workbenchRow)
duplicatePolicyPicker(workbenchRow)
}
if draft.fields.isEmpty {
Text("No canonical fields were returned for this proposal.")
.font(.system(size: 13))
.foregroundStyle(VelocityTheme.mutedFg)
.padding(14)
.frame(maxWidth: .infinity, alignment: .leading)
.background(RoundedRectangle(cornerRadius: 14).fill(VelocityTheme.surface))
} else {
VStack(alignment: .leading, spacing: 12) {
Text("Field Corrections")
.font(.system(size: 15, weight: .semibold))
.foregroundStyle(VelocityTheme.foreground)
ForEach(draft.fields, id: \.self) { field in
VStack(alignment: .leading, spacing: 7) {
Text(field.replacingOccurrences(of: "_", with: " ").uppercased())
.font(.system(size: 10, weight: .semibold))
.tracking(1.2)
.foregroundStyle(VelocityTheme.mutedFg)
TextField(field, text: Binding(
get: { fieldOverrides[field] ?? "" },
set: { fieldOverrides[field] = $0 }
))
.textFieldStyle(.plain)
.font(.system(size: 14))
.foregroundStyle(VelocityTheme.foreground)
.padding(12)
.background(RoundedRectangle(cornerRadius: 12).fill(VelocityTheme.surface))
}
}
}
}
VStack(alignment: .leading, spacing: 7) {
Text("Review Notes")
.font(.system(size: 10, weight: .semibold))
.tracking(1.2)
.foregroundStyle(VelocityTheme.mutedFg)
TextEditor(text: $notes)
.frame(minHeight: 90)
.font(.system(size: 14))
.foregroundStyle(VelocityTheme.foreground)
.scrollContentBackground(.hidden)
.padding(10)
.background(RoundedRectangle(cornerRadius: 12).fill(VelocityTheme.surface))
}
}
.padding(24)
}
.background(VelocityTheme.background)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { dismiss() }
}
ToolbarItemGroup(placement: .confirmationAction) {
Button("Needs Info") {
submit("needs_more_info")
}
Button("Approve Corrected") {
submit("approved")
}
.fontWeight(.semibold)
}
}
}
.presentationDetents([.large])
}
private func remediationDiagnostics(_ row: VelocityImportWorkbenchRowDTO) -> some View {
VStack(alignment: .leading, spacing: 12) {
Text("Validation and Duplicate Preview")
.font(.system(size: 15, weight: .semibold))
.foregroundStyle(VelocityTheme.foreground)
if row.validation.isEmpty && row.duplicateCandidates.isEmpty && row.fieldDiffs.filter(\.changed).isEmpty {
Text("No validation issues, duplicate candidates, or canonical row diffs were returned for this proposal.")
.font(.system(size: 12))
.foregroundStyle(VelocityTheme.mutedFg)
}
ForEach(row.validation) { issue in
HStack(alignment: .top, spacing: 8) {
Image(systemName: issue.severity == "error" ? "xmark.octagon" : "exclamationmark.triangle")
.foregroundStyle(issue.severity == "error" ? VelocityTheme.danger : VelocityTheme.warning)
Text("\(issue.field): \(issue.message)")
.font(.system(size: 12))
.foregroundStyle(VelocityTheme.mutedFg)
}
}
ForEach(row.duplicateCandidates.prefix(3)) { candidate in
VStack(alignment: .leading, spacing: 4) {
Text("\(candidate.fullName) · \(candidate.matchReason) match · \(candidate.matchScore)%")
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(VelocityTheme.warning)
Text([candidate.primaryPhone, candidate.primaryEmail].compactMap { $0?.trimmedNonEmpty }.joined(separator: " · "))
.font(.system(size: 11))
.foregroundStyle(VelocityTheme.mutedFg)
}
}
ForEach(row.fieldDiffs.filter(\.changed).prefix(6)) { diff in
Text("\(diff.field): \(diff.existing ?? "-")\(diff.proposed ?? "-")")
.font(.system(size: 11))
.foregroundStyle(VelocityTheme.foreground)
}
}
.padding(14)
.frame(maxWidth: .infinity, alignment: .leading)
.background(RoundedRectangle(cornerRadius: 14).fill(VelocityTheme.surface))
}
private func duplicatePolicyPicker(_ row: VelocityImportWorkbenchRowDTO) -> some View {
VStack(alignment: .leading, spacing: 10) {
Text("Duplicate Merge Policy")
.font(.system(size: 15, weight: .semibold))
.foregroundStyle(VelocityTheme.foreground)
Picker("Duplicate policy", selection: $duplicatePolicy) {
ForEach(duplicatePolicyOptions()) { policy in
Text(policy.label).tag(policy.value)
}
}
.pickerStyle(.segmented)
if let guidance = duplicatePolicyOptions().first(where: { $0.value == duplicatePolicy })?.description?.trimmedNonEmpty {
Text(guidance)
.font(.system(size: 12))
.foregroundStyle(VelocityTheme.mutedFg)
}
if row.duplicateCandidates.isEmpty {
Text("No duplicate candidates were returned by the backend for this row.")
.font(.system(size: 11))
.foregroundStyle(VelocityTheme.mutedFg)
} else if let candidate = row.duplicateCandidates.first {
Text("Strongest candidate: \(candidate.fullName) · \(candidate.matchReason) · \(candidate.matchScore)%")
.font(.system(size: 11, weight: .semibold))
.foregroundStyle(VelocityTheme.warning)
}
}
.padding(14)
.frame(maxWidth: .infinity, alignment: .leading)
.background(RoundedRectangle(cornerRadius: 14).fill(VelocityTheme.surface))
}
private func duplicatePolicyOptions() -> [VelocityVocabularyOptionDTO] {
guard !duplicatePolicies.contains(where: { $0.value == duplicatePolicy }),
let current = duplicatePolicy.trimmedNonEmpty
else {
return duplicatePolicies
}
return [
VelocityVocabularyOptionDTO(
value: current,
label: current.replacingOccurrences(of: "_", with: " ").capitalized,
description: "Current backend value",
icon: nil
)
] + duplicatePolicies
}
private func submit(_ decision: String) {
let cleanedOverrides = fieldOverrides.compactMapValues { value in
value.trimmedNonEmpty
}
let defaultNote = decision == "needs_more_info"
? "Marked needs more information from iPad Imports remediation."
: "Approved with iPad Imports field corrections."
guard let selectedPolicy = duplicatePolicy.trimmedNonEmpty else {
return
}
onSubmit(decision, notes.trimmedNonEmpty ?? defaultNote, cleanedOverrides, selectedPolicy)
dismiss()
}
}
private extension String {
var trimmedNonEmpty: String? {
let value = trimmingCharacters(in: .whitespacesAndNewlines)
return value.isEmpty ? nil : value
}
}
#Preview {
ImportsView()
}