diff --git a/KlockWork-iOS/KlockWork-iOS.xcodeproj/project.pbxproj b/KlockWork-iOS/KlockWork-iOS.xcodeproj/project.pbxproj index 6925a3e..1cd5e3d 100644 --- a/KlockWork-iOS/KlockWork-iOS.xcodeproj/project.pbxproj +++ b/KlockWork-iOS/KlockWork-iOS.xcodeproj/project.pbxproj @@ -36,6 +36,10 @@ 5387AE892C0143D100EEC581 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5387AE882C0143D100EEC581 /* SearchBar.swift */; }; 538A10312C30FB600028B785 /* ViewMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 538A10302C30FB5E0028B785 /* ViewMode.swift */; }; 538A10332C31C7D70028B785 /* RowAddButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 538A10322C31C7CC0028B785 /* RowAddButton.swift */; }; + 538A534C2C6FE8DA00711639 /* TermFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 538A534B2C6FE8D600711639 /* TermFilter.swift */; }; + 538A53502C6FEA6600711639 /* CoreDataTaxonomyTerms.swift in Sources */ = {isa = PBXBuildFile; fileRef = 538A534F2C6FEA6600711639 /* CoreDataTaxonomyTerms.swift */; }; + 538A53522C6FF1C400711639 /* TermDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 538A53512C6FF1C200711639 /* TermDetail.swift */; }; + 538A53562C7026B500711639 /* CoreDataTaxonomyTermDefinitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 538A53552C7026B500711639 /* CoreDataTaxonomyTermDefinitions.swift */; }; 53911FC52C19E87300B77DAB /* ActivityWeight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53911FC42C19E87300B77DAB /* ActivityWeight.swift */; }; 539134EA2C0269B600B48494 /* PageConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 539134E92C0269B600B48494 /* PageConfiguration.swift */; }; 539134EC2C02883000B48494 /* PersonDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 539134EB2C02883000B48494 /* PersonDetail.swift */; }; @@ -62,7 +66,6 @@ 53D4CB7E2BFE93B000AFEDEA /* Planning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D4CB6D2BFE93B000AFEDEA /* Planning.swift */; }; 53D4CB7F2BFE93B000AFEDEA /* Today.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D4CB6E2BFE93B000AFEDEA /* Today.swift */; }; 53D4CB802BFE93B000AFEDEA /* CompanyDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D4CB702BFE93B000AFEDEA /* CompanyDetail.swift */; }; - 53D4CB812BFE93B000AFEDEA /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D4CB712BFE93B000AFEDEA /* Item.swift */; }; 53D4CB822BFE93B000AFEDEA /* JobDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D4CB722BFE93B000AFEDEA /* JobDetail.swift */; }; 53D4CB832BFE93B000AFEDEA /* NoteDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D4CB732BFE93B000AFEDEA /* NoteDetail.swift */; }; 53D4CB842BFE93B000AFEDEA /* TaskDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D4CB742BFE93B000AFEDEA /* TaskDetail.swift */; }; @@ -148,6 +151,10 @@ 5387AE882C0143D100EEC581 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = ""; }; 538A10302C30FB5E0028B785 /* ViewMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewMode.swift; sourceTree = ""; }; 538A10322C31C7CC0028B785 /* RowAddButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowAddButton.swift; sourceTree = ""; }; + 538A534B2C6FE8D600711639 /* TermFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermFilter.swift; sourceTree = ""; }; + 538A534F2C6FEA6600711639 /* CoreDataTaxonomyTerms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CoreDataTaxonomyTerms.swift; path = ../../DLPrototype/DLPrototype/Models/CoreData/CoreDataTaxonomyTerms.swift; sourceTree = ""; }; + 538A53512C6FF1C200711639 /* TermDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermDetail.swift; sourceTree = ""; }; + 538A53552C7026B500711639 /* CoreDataTaxonomyTermDefinitions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CoreDataTaxonomyTermDefinitions.swift; path = ../../DLPrototype/DLPrototype/Models/CoreData/CoreDataTaxonomyTermDefinitions.swift; sourceTree = ""; }; 53911FC42C19E87300B77DAB /* ActivityWeight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ActivityWeight.swift; path = ../../DLPrototype/DLPrototype/Enums/ActivityWeight.swift; sourceTree = ""; }; 539134E92C0269B600B48494 /* PageConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageConfiguration.swift; sourceTree = ""; }; 539134EB2C02883000B48494 /* PersonDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonDetail.swift; sourceTree = ""; }; @@ -176,7 +183,6 @@ 53D4CB6D2BFE93B000AFEDEA /* Planning.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Planning.swift; sourceTree = ""; }; 53D4CB6E2BFE93B000AFEDEA /* Today.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Today.swift; sourceTree = ""; }; 53D4CB702BFE93B000AFEDEA /* CompanyDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompanyDetail.swift; sourceTree = ""; }; - 53D4CB712BFE93B000AFEDEA /* Item.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = ""; }; 53D4CB722BFE93B000AFEDEA /* JobDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JobDetail.swift; sourceTree = ""; }; 53D4CB732BFE93B000AFEDEA /* NoteDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoteDetail.swift; sourceTree = ""; }; 53D4CB742BFE93B000AFEDEA /* TaskDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TaskDetail.swift; sourceTree = ""; }; @@ -335,6 +341,7 @@ 53C059B92C33BCB20066111C /* Filter */ = { isa = PBXGroup; children = ( + 538A534B2C6FE8D600711639 /* TermFilter.swift */, 53C059B72C33BCAA0066111C /* RecordFilter.swift */, ); path = Filter; @@ -343,6 +350,8 @@ 53D4CB4A2BFE930100AFEDEA = { isa = PBXGroup; children = ( + 538A53552C7026B500711639 /* CoreDataTaxonomyTermDefinitions.swift */, + 538A534F2C6FEA6600711639 /* CoreDataTaxonomyTerms.swift */, 53911FC42C19E87300B77DAB /* ActivityWeight.swift */, 5311F4052C17C66900FB3071 /* CDAssessmentThreshold.swift */, 53E3A26A2C098A50004A85C3 /* CDAssessmentFactor.swift */, @@ -432,8 +441,8 @@ 53D4CB752BFE93B000AFEDEA /* Detail */ = { isa = PBXGroup; children = ( + 538A53512C6FF1C200711639 /* TermDetail.swift */, 53D4CB702BFE93B000AFEDEA /* CompanyDetail.swift */, - 53D4CB712BFE93B000AFEDEA /* Item.swift */, 53D4CB722BFE93B000AFEDEA /* JobDetail.swift */, 53D4CB732BFE93B000AFEDEA /* NoteDetail.swift */, 53D4CB742BFE93B000AFEDEA /* TaskDetail.swift */, @@ -583,6 +592,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 538A53562C7026B500711639 /* CoreDataTaxonomyTermDefinitions.swift in Sources */, + 538A53502C6FEA6600711639 /* CoreDataTaxonomyTerms.swift in Sources */, 53911FC52C19E87300B77DAB /* ActivityWeight.swift in Sources */, 5311F4062C17C66900FB3071 /* CDAssessmentThreshold.swift in Sources */, 53E3A26B2C098A50004A85C3 /* CDAssessmentFactor.swift in Sources */, @@ -612,6 +623,7 @@ 53F926322BFEA00E00948076 /* String.swift in Sources */, 53F926062BFE9BF300948076 /* CoreDataCalendarEvent.swift in Sources */, 53F926072BFE9BF300948076 /* CoreDataCompanies.swift in Sources */, + 538A53522C6FF1C400711639 /* TermDetail.swift in Sources */, 539134EC2C02883000B48494 /* PersonDetail.swift in Sources */, 53F926082BFE9BF300948076 /* CoreDataJob.swift in Sources */, 532441662C04428700659F14 /* Titlebar.swift in Sources */, @@ -673,11 +685,11 @@ 53D4CB842BFE93B000AFEDEA /* TaskDetail.swift in Sources */, 53E50C172C0E619C00A85FDC /* OverviewWidget.swift in Sources */, 53D4CB862BFE93B000AFEDEA /* Jobs.swift in Sources */, - 53D4CB812BFE93B000AFEDEA /* Item.swift in Sources */, 53D4CB7D2BFE93B000AFEDEA /* Explore.swift in Sources */, 53E50C192C0E61DB00A85FDC /* Legend.swift in Sources */, 539134EE2C028F6900B48494 /* ProjectDetail.swift in Sources */, 539134F82C039F3100B48494 /* Records.swift in Sources */, + 538A534C2C6FE8DA00711639 /* TermFilter.swift in Sources */, 53E6974C2C23ABA7005A7375 /* CompanySelector.swift in Sources */, 537582CC2BFFD70D00CB528C /* RecordDetail.swift in Sources */, 5311F3F92C1794FD00FB3071 /* PageActionBar.Planning.swift in Sources */, @@ -817,7 +829,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "KlockWork-iOS/KlockWork_iOS.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 17; + CURRENT_PROJECT_VERSION = 18; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"KlockWork-iOS/Preview Content\""; DEVELOPMENT_TEAM = 6DT7L2N5X6; @@ -859,7 +871,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "KlockWork-iOS/KlockWork_iOS.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 17; + CURRENT_PROJECT_VERSION = 18; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"KlockWork-iOS/Preview Content\""; DEVELOPMENT_TEAM = 6DT7L2N5X6; diff --git a/KlockWork-iOS/KlockWork-iOS/Entities/Companies.swift b/KlockWork-iOS/KlockWork-iOS/Entities/Companies.swift index bb09e07..539ecba 100644 --- a/KlockWork-iOS/KlockWork-iOS/Entities/Companies.swift +++ b/KlockWork-iOS/KlockWork-iOS/Entities/Companies.swift @@ -34,10 +34,9 @@ struct Companies: View { Text(item.name!) } } - .onDelete(perform: deleteItems) .listRowBackground(Theme.textBackground) } else { - Button(action: addItem) { + Button(action: {}) { Text("No companies found. Create one!") } .listRowBackground(Theme.textBackground) @@ -64,20 +63,3 @@ struct Companies: View { } } } - -extension Companies { - private func addItem() { - withAnimation { - let newItem = Item(timestamp: Date()) -// modelContext.insert(newItem) - } - } - - private func deleteItems(offsets: IndexSet) { - withAnimation { - for index in offsets { -// modelContext.delete(items[index]) - } - } - } -} diff --git a/KlockWork-iOS/KlockWork-iOS/Entities/Detail/Item.swift b/KlockWork-iOS/KlockWork-iOS/Entities/Detail/Item.swift deleted file mode 100644 index 054ddea..0000000 --- a/KlockWork-iOS/KlockWork-iOS/Entities/Detail/Item.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// Item.swift -// KlockWorkiOS -// -// Created by Ryan Priebe on 2024-05-18. -// Copyright © 2024 YegCollective. All rights reserved. -// - -import Foundation -import SwiftData - -@Model -final class Item { - var timestamp: Date - - init(timestamp: Date) { - self.timestamp = timestamp - } -} diff --git a/KlockWork-iOS/KlockWork-iOS/Entities/Detail/TermDetail.swift b/KlockWork-iOS/KlockWork-iOS/Entities/Detail/TermDetail.swift new file mode 100644 index 0000000..a973787 --- /dev/null +++ b/KlockWork-iOS/KlockWork-iOS/Entities/Detail/TermDetail.swift @@ -0,0 +1,150 @@ +// +// TermDetail.swift +// KlockWork-iOS +// +// Created by Ryan Priebe on 2024-08-16. +// + +import SwiftUI + +struct TermDetail: View { + typealias DefinitionLink = Tabs.Content.Individual.SingleJobLink + + @EnvironmentObject private var state: AppState + @Environment(\.dismiss) private var dismiss + public var term: TaxonomyTerm? + @State private var created: Date = Date() + @State private var name: String = "" + @State private var definitions: [TaxonomyTermDefinitions] = [] + @State private var alive: Bool = false + @State private var isSaveAlertPresented: Bool = false + @State private var isDeleteAlertPresented: Bool = false + public var page: PageConfiguration.AppPage = .create + + var body: some View { + VStack { + List { + Section("Term") { + TextField("Name", text: $name, axis: .vertical) + } + .listRowBackground(Theme.textBackground) + + Section("Definitions") { + ForEach(self.definitions, id: \TaxonomyTermDefinitions.objectID) { definition in + if definition.job != nil { + DefinitionLink(job: definition.job!) + } + } + } + .listRowBackground(Theme.textBackground) + + Section("Settings") { + Toggle("Published", isOn: $alive) + DatePicker( + "Created", + selection: $created, + displayedComponents: [.date, .hourAndMinute] + ) + // @TODO: implement JobPicker as a sheet + } + .listRowBackground(Theme.textBackground) + + if self.term != nil { + Button("Delete Term", role: .destructive, action: self.actionInitiateDelete) + .alert("Are you sure?", isPresented: $isDeleteAlertPresented) { + Button("Yes", role: .destructive) { + self.actionOnDelete() + } + } message: { + Text("This term will be permanently deleted.") + } + .listRowBackground(Color.red) + .foregroundStyle(.white) + } + } + Spacer() + } + .background(self.page.primaryColour) + .onAppear(perform: actionOnAppear) + .toolbarBackground(Theme.textBackground.opacity(0.7), for: .navigationBar) + .toolbarBackground(.visible, for: .navigationBar) + .scrollContentBackground(.hidden) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + // Creates new entity on tap, then sends user back to Today + Button { + self.actionOnSave() + } label: { + Text("Save") + } + .foregroundStyle(self.state.theme.tint) + .alert("Saved", isPresented: $isSaveAlertPresented) { + Button("OK") { + dismiss() + } + } message: { + Text("It is done.") + } + } + } + } +} + +extension TermDetail { + /// Onload handler. Sets timestamp and message fields. + /// - Returns: Void + private func actionOnAppear() -> Void { + if self.term != nil { + if let tmstmp = self.term!.created { + self.created = tmstmp + } + + if let name = self.term!.name { + self.name = name + } + + if let def = self.term!.definitions { + self.definitions = def.allObjects as! [TaxonomyTermDefinitions] + } + + self.alive = self.term!.alive + } + } + + /// Save handler + /// - Returns: Void + private func actionOnSave() -> Void { + if self.term != nil { + self.term!.name = self.name +// self.term!.definitions = self.definition + self.term!.alive = self.alive + } else { +// CoreDataTaxonomyTerms(moc: self.state.moc).create( +// message: self.message, +// timestamp: self.timestamp, +// job: self.job, +// saveByDefault: false +// ) + } + + isSaveAlertPresented.toggle() + PersistenceController.shared.save() + } + + /// Soft delete a Task + /// - Returns: Void + private func actionOnDelete() -> Void { + if self.term != nil { + self.state.moc.delete(self.term!) + } + + PersistenceController.shared.save() + dismiss() + } + + /// Opens the delete object alert + /// - Returns: Void + private func actionInitiateDelete() -> Void { + self.isDeleteAlertPresented.toggle() + } +} diff --git a/KlockWork-iOS/KlockWork-iOS/Entities/Filter/TermFilter.swift b/KlockWork-iOS/KlockWork-iOS/Entities/Filter/TermFilter.swift new file mode 100644 index 0000000..a4da6a3 --- /dev/null +++ b/KlockWork-iOS/KlockWork-iOS/Entities/Filter/TermFilter.swift @@ -0,0 +1,86 @@ +// +// TermFilter.swift +// KlockWork-iOS +// +// Created by Ryan Priebe on 2024-08-16. +// + +import SwiftUI + +struct TermsGroupedByDate: Identifiable { + var id: UUID = UUID() + var date: Date + var definitions: [TaxonomyTermDefinitions] +} + +struct GroupedTermDateRow: View { + public let date: Date + + var body: some View { + HStack(alignment: .center, spacing: 5) { + Image(systemName: "calendar") + Text(date.formatted(date: .abbreviated, time: .omitted)) + Spacer() + } + .padding(8) + } +} + +struct TermFilter: View { + typealias Button = Tabs.Content.Individual.SingleTerm + public let job: Job + public var page: PageConfiguration.AppPage = .create + @FetchRequest private var definitions: FetchedResults + @State private var grouped: [TermsGroupedByDate] = [] + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + ScrollView(showsIndicators: false) { + ForEach(grouped.sorted(by: {$0.date > $1.date})) { group in + VStack(alignment: .leading, spacing: 1) { + GroupedTermDateRow(date: group.date) + + ForEach(group.definitions) { definition in + if definition.term != nil { + Button(term: definition.term!) + } + } + } + } + } + } + .onAppear(perform: self.actionOnAppear) + .navigationTitle("Taxonomy Terms") + .background(self.page.primaryColour) + .scrollContentBackground(.hidden) + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(Theme.textBackground.opacity(0.7), for: .navigationBar) + .toolbarBackground(.visible, for: .navigationBar) + .scrollDismissesKeyboard(.immediately) + } + + init(job: Job) { + self.job = job + _definitions = CoreDataTaxonomyTerms.fetchDefinitions(job: self.job) + } +} + +extension TermFilter { + /// Onload handler + /// - Returns: Void + private func actionOnAppear() -> Void { + self.grouped = [] + if self.definitions.count > 0 { + let sortedRecords = Array(self.definitions) + .sliced(by: [.year, .month, .day], for: \.created!) + .sorted(by: {$0.key > $1.key}) + let grouped = Dictionary(grouping: sortedRecords, by: {$0.key}) + + for group in grouped { + self.grouped.append( + TermsGroupedByDate(date: group.key, definitions: group.value.first?.value ?? []) + ) + } + } + } +} diff --git a/KlockWork-iOS/KlockWork-iOS/Entities/People.swift b/KlockWork-iOS/KlockWork-iOS/Entities/People.swift index 70b2556..5f41625 100644 --- a/KlockWork-iOS/KlockWork-iOS/Entities/People.swift +++ b/KlockWork-iOS/KlockWork-iOS/Entities/People.swift @@ -69,7 +69,7 @@ struct People: View { extension People { private func addItem() { withAnimation { - let newItem = Item(timestamp: Date()) +// let newItem = Item(timestamp: Date()) // modelContext.insert(newItem) } } diff --git a/KlockWork-iOS/KlockWork-iOS/Entities/Projects.swift b/KlockWork-iOS/KlockWork-iOS/Entities/Projects.swift index d67e4e7..562adb5 100644 --- a/KlockWork-iOS/KlockWork-iOS/Entities/Projects.swift +++ b/KlockWork-iOS/KlockWork-iOS/Entities/Projects.swift @@ -70,7 +70,7 @@ struct Projects: View { extension Projects { private func addItem() { withAnimation { - let newItem = Item(timestamp: Date()) +// let newItem = Item(timestamp: Date()) // modelContext.insert(newItem) } } diff --git a/KlockWork-iOS/KlockWork-iOS/Entities/Records.swift b/KlockWork-iOS/KlockWork-iOS/Entities/Records.swift index aba5898..42aaae3 100644 --- a/KlockWork-iOS/KlockWork-iOS/Entities/Records.swift +++ b/KlockWork-iOS/KlockWork-iOS/Entities/Records.swift @@ -34,10 +34,9 @@ struct Records: View { Text(item.message!) } } - .onDelete(perform: deleteItems) .listRowBackground(Theme.textBackground) } else { - Button(action: addItem) { + Button(action: {}) { Text("No people found. Create one!") } .listRowBackground(Theme.textBackground) @@ -64,20 +63,3 @@ struct Records: View { } } } - -extension Records { - private func addItem() { - withAnimation { - let newItem = Item(timestamp: Date()) -// modelContext.insert(newItem) - } - } - - private func deleteItems(offsets: IndexSet) { - withAnimation { - for index in offsets { -// modelContext.delete(items[index]) - } - } - } -} diff --git a/KlockWork-iOS/KlockWork-iOS/Entities/Tasks.swift b/KlockWork-iOS/KlockWork-iOS/Entities/Tasks.swift index bcd4904..f2aa109 100644 --- a/KlockWork-iOS/KlockWork-iOS/Entities/Tasks.swift +++ b/KlockWork-iOS/KlockWork-iOS/Entities/Tasks.swift @@ -70,7 +70,7 @@ struct Tasks: View { extension Tasks { private func addItem() { withAnimation { - let newItem = Item(timestamp: Date()) +// let newItem = Item(timestamp: Date()) // modelContext.insert(newItem) } } diff --git a/KlockWork-iOS/KlockWork-iOS/Libraries/SearchLibrary.swift b/KlockWork-iOS/KlockWork-iOS/Libraries/SearchLibrary.swift index fdd7881..5b14544 100644 --- a/KlockWork-iOS/KlockWork-iOS/Libraries/SearchLibrary.swift +++ b/KlockWork-iOS/KlockWork-iOS/Libraries/SearchLibrary.swift @@ -73,6 +73,7 @@ extension SearchLibrary.SearchEngine { results.add(await ProjectsEntityView(entityType: .projects, term: self.term!)) results.add(await RecordsEntityView(entityType: .records, term: self.term!)) results.add(await TasksEntityView(entityType: .tasks, term: self.term!)) + results.add(await TermEntityView(entityType: .terms, term: self.term!)) return results } @@ -292,4 +293,34 @@ extension SearchLibrary.SearchEngine { _results = CoreDataTasks.fetchMatching(term: term) } } + + fileprivate struct TermEntityView: View { + typealias Row = Tabs.Content.Individual.SingleTerm + typealias EntityType = PageConfiguration.EntityType + + @State public var entityType: EntityType + @State private var open: Bool = true + @FetchRequest public var results: FetchedResults + + var body: some View { + VStack(alignment: .leading, spacing: 1) { + TitleBar(selected: $entityType, open: $open, count: results.count) + + if open { + if !results.isEmpty { + ForEach(results) { entity in + Row(term: entity) + } + } else { + StatusMessage.Warning(message: "No tasks matched") + } + } + } + } + + init(entityType: EntityType, term: String) { + self.entityType = entityType + _results = CoreDataTaxonomyTerms.fetchMatching(term: term) + } + } } diff --git a/KlockWork-iOS/KlockWork-iOS/Main.swift b/KlockWork-iOS/KlockWork-iOS/Main.swift index 9359e48..3617b14 100644 --- a/KlockWork-iOS/KlockWork-iOS/Main.swift +++ b/KlockWork-iOS/KlockWork-iOS/Main.swift @@ -83,4 +83,52 @@ extension Main { self.state.activities.statuses = allStatuses self.state.activities.assess() } + + /// Rebuilds terms and definitions + /// - Returns: Void + private func recreateTermsAndDefinitionsFromRecords() -> Void { + let records = CoreDataRecords(moc: self.moc).matching(/(.*) == (.*)/) + var terms = CoreDataTaxonomyTerms(moc: self.moc).all() + var definitions = CoreDataTaxonomyTermDefinitions(moc: self.moc).all() + + // reset taxonomy terms + for term in terms { + self.moc.delete(term) + } + + for definition in definitions { + self.moc.delete(definition) + } + + for record in records { + if let matches = record.message?.matches(of: /(.*) == (.*)/) { + for match in matches { + let def = TaxonomyTermDefinitions(context: self.moc) + def.alive = true + def.created = record.timestamp + def.job = record.job + def.definition = String(match.2) + + if let foundTerm = CoreDataTaxonomyTerms(moc: self.moc).byName(String(match.1)) { + if let foundDefs = foundTerm.definitions?.allObjects as? [TaxonomyTermDefinitions] { + for fDef in foundDefs { + if def.definition != fDef.definition { + foundTerm.addToDefinitions(def) + } + } + } + } else { + let term = TaxonomyTerm(context: moc) + term.alive = true + term.created = record.timestamp + term.lastUpdate = record.timestamp + term.name = String(match.1) + term.addToDefinitions(def) + } + } + } + } + + PersistenceController.shared.save() + } } diff --git a/KlockWork-iOS/KlockWork-iOS/Pages/Explore.swift b/KlockWork-iOS/KlockWork-iOS/Pages/Explore.swift index 6efd239..aff95c9 100644 --- a/KlockWork-iOS/KlockWork-iOS/Pages/Explore.swift +++ b/KlockWork-iOS/KlockWork-iOS/Pages/Explore.swift @@ -29,8 +29,10 @@ struct Explore: View { Divider().background(.white).frame(height: 1) Widgets(text: $searchText) } - .navigationBarTitleDisplayMode(.inline) .background(page.primaryColour) + .navigationBarTitleDisplayMode(.inline) + .toolbar(.hidden) + .toolbarBackground(Theme.textBackground.opacity(0.7), for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar) .scrollDismissesKeyboard(.immediately) } diff --git a/KlockWork-iOS/KlockWork-iOS/SharedEnums/PageConfiguration.swift b/KlockWork-iOS/KlockWork-iOS/SharedEnums/PageConfiguration.swift index 68c933b..d486c47 100644 --- a/KlockWork-iOS/KlockWork-iOS/SharedEnums/PageConfiguration.swift +++ b/KlockWork-iOS/KlockWork-iOS/SharedEnums/PageConfiguration.swift @@ -49,7 +49,7 @@ extension PageConfiguration { } enum EntityType: CaseIterable, Equatable { - case records, tasks, notes, people, companies, projects, jobs + case records, tasks, notes, people, companies, projects, jobs, terms /// Interface-friendly representation var label: String { @@ -61,6 +61,7 @@ extension PageConfiguration { case .companies: "Companies" case .people: "People" case .projects: "Projects" + case .terms: "Terms" } } @@ -74,6 +75,7 @@ extension PageConfiguration { case .companies: "Company" case .people: "Person" case .projects: "Project" + case .terms: "Term" } } @@ -87,6 +89,7 @@ extension PageConfiguration { case .companies: Image(systemName: "building.2") case .people: Image(systemName: "person.2") case .projects: Image(systemName: "folder") + case .terms: Image(systemName: "list.bullet.rectangle") } } @@ -99,6 +102,7 @@ extension PageConfiguration { case .companies: Image(systemName: "building.2.fill") case .people: Image(systemName: "person.2.fill") case .projects: Image(systemName: "folder.fill") + case .terms: Image(systemName: "list.bullet.rectangle.fill") } } } diff --git a/KlockWork-iOS/KlockWork-iOS/SharedViews/SearchBar.swift b/KlockWork-iOS/KlockWork-iOS/SharedViews/SearchBar.swift index 1930910..10a4465 100644 --- a/KlockWork-iOS/KlockWork-iOS/SharedViews/SearchBar.swift +++ b/KlockWork-iOS/KlockWork-iOS/SharedViews/SearchBar.swift @@ -172,6 +172,18 @@ extension SearchBar { } .listRowBackground(Theme.textBackground) } + case .terms: + let group = items as! [TaxonomyTerm] + ForEach(group.filter { + $0.alive == true && $0.name!.lowercased().contains(text.lowercased()) + }) { row in + NavigationLink { + TermDetail(term: row) + } label: { + Text(row.name!) + } + .listRowBackground(Theme.textBackground) + } } } } diff --git a/KlockWork-iOS/KlockWork-iOS/SharedViews/Tabs.swift b/KlockWork-iOS/KlockWork-iOS/SharedViews/Tabs.swift index a012e46..8670098 100644 --- a/KlockWork-iOS/KlockWork-iOS/SharedViews/Tabs.swift +++ b/KlockWork-iOS/KlockWork-iOS/SharedViews/Tabs.swift @@ -95,7 +95,7 @@ extension Tabs { HStack(alignment: .center, spacing: 1) { // @TODO: restore to original state (below) // ForEach(EntityType.allCases, id: \.self) { page in - ForEach(EntityType.allCases, id: \.self) { page in + ForEach(EntityType.allCases.filter({$0 != .terms}), id: \.self) { page in VStack { Button { withAnimation(.bouncy(duration: Tabs.animationDuration)) { @@ -129,19 +129,21 @@ extension Tabs { var body: some View { switch selected { case .records: - List.Records(job: $job, date: self.state.date, inSheet: inSheet) + List.Records(job: $job, date: self.state.date, inSheet: self.inSheet) case .jobs: - List.Jobs(job: $job, date: self.state.date, inSheet: inSheet) + List.Jobs(job: $job, date: self.state.date, inSheet: self.inSheet) case .tasks: - List.Tasks(date: self.state.date, inSheet: inSheet) + List.Tasks(date: self.state.date, inSheet: self.inSheet) case .notes: - List.Notes(date: self.state.date, inSheet: inSheet) + List.Notes(date: self.state.date, inSheet: self.inSheet) case .companies: - List.Companies(date: self.state.date, inSheet: inSheet) + List.Companies(date: self.state.date, inSheet: self.inSheet) case .people: - List.People(date: self.state.date, inSheet: inSheet) + List.People(date: self.state.date, inSheet: self.inSheet) case .projects: - List.Projects(date: self.state.date, inSheet: inSheet) + List.Projects(date: self.state.date, inSheet: self.inSheet) + case .terms: + List.Terms(inSheet: self.inSheet, entity: $job.wrappedValue!) } } } @@ -427,7 +429,7 @@ extension Tabs.Content { /// Quick task creator if self.isCreateTaskPanelPresented { - VStack { + VStack(alignment: .leading, spacing: 0) { HStack(alignment: .center, spacing: 0) { Rectangle() .foregroundStyle(Color.fromStored(self.entity.project?.company?.colour ?? Theme.rowColourAsDouble)) @@ -492,7 +494,7 @@ extension Tabs.Content { /// Quick note creator if self.isCreateNotePanelPresented { - VStack { + VStack(alignment: .leading, spacing: 0) { HStack(alignment: .center, spacing: 0) { Rectangle() .foregroundStyle(Color.fromStored(self.entity.project?.company?.colour ?? Theme.rowColourAsDouble)) @@ -522,6 +524,9 @@ extension Tabs.Content { } } + // Terms + Terms(inSheet: false, entity: self.entity) + /// Record view link ZStack(alignment: .leading) { self.entity.backgroundColor @@ -698,6 +703,82 @@ extension Tabs.Content { } } + struct Terms: View { + public var inSheet: Bool + public let entity: Job + @State private var isCreateTaskPanelPresented: Bool = false + @State private var items: [TaxonomyTerm] = [] // @TODO: we maaaaay only use this to determine term count + @State private var newTermName: String = "" + @State private var newTermDefinition: String = "" + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + ZStack(alignment: .leading) { + self.entity.backgroundColor + LinearGradient(gradient: Gradient(colors: [Theme.base, .clear]), startPoint: .trailing, endPoint: .leading) + .opacity(0.6) + .blendMode(.softLight) + .frame(height: 50) + VStack(alignment: .leading, spacing: 0) { + HStack(alignment: .center, spacing: 0) { + Rectangle() + .foregroundStyle(Color.fromStored(self.entity.project?.company?.colour ?? Theme.rowColourAsDouble)) + .frame(width: 15) + Rectangle() + .foregroundStyle(Color.fromStored(self.entity.project?.colour ?? Theme.rowColourAsDouble)) + .frame(width: 15) + if self.items.isEmpty { + Rectangle() + .foregroundStyle(Color.fromStored(self.entity.colour ?? Theme.rowColourAsDouble)) + .frame(width: 15) + } + + HStack(spacing: 0) { + if self.items.isEmpty { + Text("No Terms") + .opacity(0.5) + } else { + NavigationLink { + TermFilter(job: self.entity) + } label: { + ListRow(name: self.items.count == 1 ? "1 Term" : "\(self.items.count) Terms", colour: self.entity.backgroundColor) + } + } + } + .padding(.leading, 8) + } + } + } + } + .onAppear(perform: self.actionOnAppear) + } + + /// Onload handler + /// - Returns: Void + private func actionOnAppear() -> Void { + self.items = [] + if let definitions = self.entity.definitions?.allObjects as? [TaxonomyTermDefinitions] { + for definition in definitions { + if let term = definition.term { + self.items.append(term) + } + } + } + } + + /// OnCreate handler + /// - Returns: Void + private func actionOnCreateTerm() -> Void { + + } + + /// Post-save handler + /// - Returns: Void + private func actionPostSave() -> Void { + + } + } + struct Companies: View { public var inSheet: Bool @FetchRequest private var items: FetchedResults @@ -832,6 +913,55 @@ extension Tabs.Content { } } + struct SingleTerm: View { + @EnvironmentObject private var state: AppState + public let term: TaxonomyTerm + @State private var definitions: [TaxonomyTermDefinitions] = [] + @State private var colour: Color = Theme.rowColour + + var body: some View { + NavigationLink { + TermDetail(term: self.term) + } label: { + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 0) { + Text(term.name ?? "_TERM_NAME") + .font(.title3) + .fontWeight(.heavy) + .multilineTextAlignment(.leading) + Spacer() + Image(systemName: "chevron.right") + .foregroundStyle(.gray) + } + .padding(10) + + VStack(alignment: .leading, spacing: 0) { + ForEach(self.definitions, id: \TaxonomyTermDefinitions.objectID) { term in + HStack(alignment: .top) { + Text("1. ") + Text(term.definition ?? "_TERM_DEFINITION") + .multilineTextAlignment(.leading) + Spacer() + } + .padding(8) + .background(term.job?.backgroundColor) + .foregroundStyle(term.job != nil ? term.job!.backgroundColor.isBright() ? .black : .white : .white) + } + } + } + .background(Theme.rowColour) + } + .onAppear(perform: self.actionOnAppear) + // @TODO: use .onLongPressGesture to open record inspector view, allowing job selection and other functions + } + + /// Onload handler + /// - Returns: Void + private func actionOnAppear() -> Void { + self.definitions = self.term.definitions?.allObjects as! [TaxonomyTermDefinitions] + } + } + struct SingleJob: View { public let job: Job @Binding public var stateJob: Job? @@ -1174,6 +1304,7 @@ extension Tabs.Content { .foregroundStyle(Color.fromStored(self.entity.colour ?? Theme.rowColourAsDouble).isBright() ? Theme.base : .white) .opacity(0.7) .padding(.leading, 8) + .multilineTextAlignment(.leading) Spacer() // @TODO: uncomment after we list out people under projects // RowAddNavLink( diff --git a/KlockWork-iOS/KlockWork-iOS/SharedViews/Widget/DataExplorer.swift b/KlockWork-iOS/KlockWork-iOS/SharedViews/Widget/DataExplorer.swift index 51c0981..2342cb8 100644 --- a/KlockWork-iOS/KlockWork-iOS/SharedViews/Widget/DataExplorer.swift +++ b/KlockWork-iOS/KlockWork-iOS/SharedViews/Widget/DataExplorer.swift @@ -77,6 +77,9 @@ extension Widget.DataExplorer { case .projects: Projects() .navigationTitle(type.label) + case .terms: + EmptyView() // @TODO: implement + .navigationTitle(type.label) } } label: { HStack { @@ -114,6 +117,8 @@ extension Widget.DataExplorer { count = CoreDataTasks(moc: self.state.moc).countAllTime() case .projects: count = CoreDataProjects(moc: self.state.moc).countAll() + case .terms: + count = CoreDataTaxonomyTerms(moc: self.state.moc).countAll() } entityCounts.append(EntityTypePair(key: type, value: count))