summaryrefslogtreecommitdiff
path: root/DomainDig/DomainViewModel+Audit.swift
blob: a825b608ed095230f9d32a4713e029e7c412f1b5 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
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)
    }
}