diff options
| -rw-r--r-- | DomainDig/DomainViewModel+Audit.swift | 114 | ||||
| -rw-r--r-- | DomainDig/DomainViewModel.swift | 105 |
2 files changed, 115 insertions, 104 deletions
diff --git a/DomainDig/DomainViewModel+Audit.swift b/DomainDig/DomainViewModel+Audit.swift new file mode 100644 index 0000000..a825b60 --- /dev/null +++ b/DomainDig/DomainViewModel+Audit.swift @@ -0,0 +1,114 @@ +import Foundation +import SwiftUI + +/// Audit review surface of `DomainViewModel`: reading audit sessions and their +/// timeline, and mutating a session's status, notes, checklist, and findings. +/// +/// `startAudit(for:)` deliberately stays on the main type — it drives a live +/// inspection to seed the session, so it belongs with the inspection pipeline +/// until that is extracted. Everything here operates purely on `auditSessions` +/// and persists through `persistAuditSessions()`. +extension DomainViewModel { + func audits(for domain: String) -> [AuditSession] { + auditSessions + .filter { $0.domain.caseInsensitiveCompare(domain) == .orderedSame } + .sorted { $0.createdAt > $1.createdAt } + } + + func auditSession(withID id: UUID) -> AuditSession? { + auditSessions.first(where: { $0.id == id }) + } + + func auditTimeline(for domain: String) -> [AuditTimelinePoint] { + let sessions = audits(for: domain).sorted { $0.createdAt > $1.createdAt } + return sessions.map { session in + let repeatedIssues = sessions + .filter { $0.id != session.id } + .flatMap(\.findings) + .map { $0.title.lowercased() } + let repeatedIssueCount = session.findings.filter { + repeatedIssues.contains($0.title.lowercased()) + }.count + + return AuditTimelinePoint( + id: session.id, + sessionID: session.id, + domain: session.domain, + createdAt: session.createdAt, + status: session.status, + findingCount: session.findings.count, + openHighSeverityCount: session.findings.filter { $0.severity == .high && $0.status != .resolved }.count, + repeatedIssueCount: repeatedIssueCount + ) + } + } + + func updateAuditStatus(_ status: AuditStatus, sessionID: UUID) { + guard let index = auditSessions.firstIndex(where: { $0.id == sessionID }) else { return } + auditSessions[index].status = status + persistAuditSessions() + } + + func updateAuditNotes(_ notes: String, sessionID: UUID) { + guard let index = auditSessions.firstIndex(where: { $0.id == sessionID }) else { return } + auditSessions[index].notes = notes.trimmingCharacters(in: .whitespacesAndNewlines) + persistAuditSessions() + } + + func toggleAuditChecklistItem(sessionID: UUID, itemID: UUID) { + guard let sessionIndex = auditSessions.firstIndex(where: { $0.id == sessionID }), + let itemIndex = auditSessions[sessionIndex].checklist.firstIndex(where: { $0.id == itemID }) else { + return + } + + auditSessions[sessionIndex].checklist[itemIndex].isComplete.toggle() + auditSessions[sessionIndex].checklist[itemIndex].completedAt = auditSessions[sessionIndex].checklist[itemIndex].isComplete ? Date() : nil + persistAuditSessions() + } + + func addAuditFinding( + sessionID: UUID, + title: String, + severity: AuditFindingSeverity, + summary: String, + evidenceReferences: [String], + notes: String, + checklistAreas: [AuditChecklistArea] + ) { + guard let index = auditSessions.firstIndex(where: { $0.id == sessionID }) else { return } + let finding = AuditFinding( + title: title, + severity: severity, + summary: summary, + evidenceReferences: evidenceReferences, + notes: notes, + status: .open, + checklistAreas: checklistAreas + ) + auditSessions[index].findings.insert(finding, at: 0) + persistAuditSessions() + } + + func updateAuditFinding(_ finding: AuditFinding, sessionID: UUID) { + guard let sessionIndex = auditSessions.firstIndex(where: { $0.id == sessionID }), + let findingIndex = auditSessions[sessionIndex].findings.firstIndex(where: { $0.id == finding.id }) else { + return + } + + var updatedFinding = finding + updatedFinding.updatedAt = Date() + auditSessions[sessionIndex].findings[findingIndex] = updatedFinding + persistAuditSessions() + } + + func removeAuditFindings(at offsets: IndexSet, sessionID: UUID) { + guard let sessionIndex = auditSessions.firstIndex(where: { $0.id == sessionID }) else { return } + auditSessions[sessionIndex].findings.remove(atOffsets: offsets) + persistAuditSessions() + } + + func exportAuditData(sessionID: UUID, format: AuditExportFormat) -> Data? { + guard let session = auditSession(withID: sessionID) else { return nil } + return try? AuditExporter.data(for: session, format: format) + } +} diff --git a/DomainDig/DomainViewModel.swift b/DomainDig/DomainViewModel.swift index b5f51c3..fdf6b84 100644 --- a/DomainDig/DomainViewModel.swift +++ b/DomainDig/DomainViewModel.swift @@ -1546,40 +1546,6 @@ final class DomainViewModel { return try? DomainReportExporter.data(for: currentReport, format: format) } - func audits(for domain: String) -> [AuditSession] { - auditSessions - .filter { $0.domain.caseInsensitiveCompare(domain) == .orderedSame } - .sorted { $0.createdAt > $1.createdAt } - } - - func auditSession(withID id: UUID) -> AuditSession? { - auditSessions.first(where: { $0.id == id }) - } - - func auditTimeline(for domain: String) -> [AuditTimelinePoint] { - let sessions = audits(for: domain).sorted { $0.createdAt > $1.createdAt } - return sessions.map { session in - let repeatedIssues = sessions - .filter { $0.id != session.id } - .flatMap(\.findings) - .map { $0.title.lowercased() } - let repeatedIssueCount = session.findings.filter { - repeatedIssues.contains($0.title.lowercased()) - }.count - - return AuditTimelinePoint( - id: session.id, - sessionID: session.id, - domain: session.domain, - createdAt: session.createdAt, - status: session.status, - findingCount: session.findings.count, - openHighSeverityCount: session.findings.filter { $0.severity == .high && $0.status != .resolved }.count, - repeatedIssueCount: repeatedIssueCount - ) - } - } - @discardableResult func startAudit(for domain: String, reviewer: String? = nil) async -> AuditSession? { let normalizedDomain = domain @@ -1625,75 +1591,6 @@ final class DomainViewModel { return session } - func updateAuditStatus(_ status: AuditStatus, sessionID: UUID) { - guard let index = auditSessions.firstIndex(where: { $0.id == sessionID }) else { return } - auditSessions[index].status = status - persistAuditSessions() - } - - func updateAuditNotes(_ notes: String, sessionID: UUID) { - guard let index = auditSessions.firstIndex(where: { $0.id == sessionID }) else { return } - auditSessions[index].notes = notes.trimmingCharacters(in: .whitespacesAndNewlines) - persistAuditSessions() - } - - func toggleAuditChecklistItem(sessionID: UUID, itemID: UUID) { - guard let sessionIndex = auditSessions.firstIndex(where: { $0.id == sessionID }), - let itemIndex = auditSessions[sessionIndex].checklist.firstIndex(where: { $0.id == itemID }) else { - return - } - - auditSessions[sessionIndex].checklist[itemIndex].isComplete.toggle() - auditSessions[sessionIndex].checklist[itemIndex].completedAt = auditSessions[sessionIndex].checklist[itemIndex].isComplete ? Date() : nil - persistAuditSessions() - } - - func addAuditFinding( - sessionID: UUID, - title: String, - severity: AuditFindingSeverity, - summary: String, - evidenceReferences: [String], - notes: String, - checklistAreas: [AuditChecklistArea] - ) { - guard let index = auditSessions.firstIndex(where: { $0.id == sessionID }) else { return } - let finding = AuditFinding( - title: title, - severity: severity, - summary: summary, - evidenceReferences: evidenceReferences, - notes: notes, - status: .open, - checklistAreas: checklistAreas - ) - auditSessions[index].findings.insert(finding, at: 0) - persistAuditSessions() - } - - func updateAuditFinding(_ finding: AuditFinding, sessionID: UUID) { - guard let sessionIndex = auditSessions.firstIndex(where: { $0.id == sessionID }), - let findingIndex = auditSessions[sessionIndex].findings.firstIndex(where: { $0.id == finding.id }) else { - return - } - - var updatedFinding = finding - updatedFinding.updatedAt = Date() - auditSessions[sessionIndex].findings[findingIndex] = updatedFinding - persistAuditSessions() - } - - func removeAuditFindings(at offsets: IndexSet, sessionID: UUID) { - guard let sessionIndex = auditSessions.firstIndex(where: { $0.id == sessionID }) else { return } - auditSessions[sessionIndex].findings.remove(atOffsets: offsets) - persistAuditSessions() - } - - func exportAuditData(sessionID: UUID, format: AuditExportFormat) -> Data? { - guard let session = auditSession(withID: sessionID) else { return nil } - return try? AuditExporter.data(for: session, format: format) - } - func loadOwnershipHistory() async { guard !searchedDomain.isEmpty else { return } guard DataAccessService.hasAccess(to: .ownershipHistory) else { @@ -2605,7 +2502,7 @@ final class DomainViewModel { DomainDebugLog.signpostEnd("DomainViewModel.persistHistory", start: persistStartedAt, extra: "count=\(history.count)") } - private func persistAuditSessions() { + func persistAuditSessions() { DomainDataPortabilityService.saveAuditSessions(auditSessions) refreshDataLifecycleSummary() } |
