summaryrefslogtreecommitdiff
path: root/DomainDig/DomainViewModel+Audit.swift
diff options
context:
space:
mode:
Diffstat (limited to 'DomainDig/DomainViewModel+Audit.swift')
-rw-r--r--DomainDig/DomainViewModel+Audit.swift114
1 files changed, 114 insertions, 0 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)
+ }
+}