156 lines
4.5 KiB
Swift
156 lines
4.5 KiB
Swift
import Foundation
|
|
import Observation
|
|
import SwiftData
|
|
|
|
@Observable
|
|
@MainActor
|
|
final class ContactsStore: ContactTracking {
|
|
private let modelContext: ModelContext
|
|
private(set) var contacts: [Contact] = []
|
|
var searchQuery: String = ""
|
|
|
|
init(modelContext: ModelContext) {
|
|
self.modelContext = modelContext
|
|
fetchContacts()
|
|
}
|
|
|
|
func fetchContacts() {
|
|
let descriptor = FetchDescriptor<Contact>(
|
|
sortBy: [SortDescriptor(\.lastSharedDate, order: .reverse)]
|
|
)
|
|
do {
|
|
contacts = try modelContext.fetch(descriptor)
|
|
} catch {
|
|
contacts = []
|
|
}
|
|
}
|
|
|
|
var visibleContacts: [Contact] {
|
|
let trimmedQuery = searchQuery.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !trimmedQuery.isEmpty else { return contacts }
|
|
return contacts.filter { contact in
|
|
contact.name.localizedStandardContains(trimmedQuery)
|
|
|| contact.company.localizedStandardContains(trimmedQuery)
|
|
|| contact.role.localizedStandardContains(trimmedQuery)
|
|
|| contact.tags.localizedStandardContains(trimmedQuery)
|
|
|| contact.notes.localizedStandardContains(trimmedQuery)
|
|
}
|
|
}
|
|
|
|
/// Contacts with overdue follow-ups
|
|
var overdueFollowUps: [Contact] {
|
|
contacts.filter { $0.isFollowUpOverdue }
|
|
}
|
|
|
|
/// Contacts that were received (scanned from others)
|
|
var receivedCards: [Contact] {
|
|
contacts.filter { $0.isReceivedCard }
|
|
}
|
|
|
|
func recordShare(for name: String, role: String, company: String, cardLabel: String) {
|
|
// Check if contact already exists
|
|
if let existingContact = contacts.first(where: { $0.name == name && $0.company == company }) {
|
|
existingContact.lastSharedDate = .now
|
|
existingContact.cardLabel = cardLabel
|
|
} else {
|
|
let newContact = Contact(
|
|
name: name,
|
|
role: role,
|
|
company: company,
|
|
avatarSystemName: "person.crop.circle",
|
|
lastSharedDate: .now,
|
|
cardLabel: cardLabel
|
|
)
|
|
modelContext.insert(newContact)
|
|
}
|
|
saveContext()
|
|
fetchContacts()
|
|
}
|
|
|
|
/// Adds a contact from a received vCard (scanned QR code)
|
|
func addReceivedCard(vCardData: String) {
|
|
let contact = Contact.fromVCard(vCardData)
|
|
modelContext.insert(contact)
|
|
saveContext()
|
|
fetchContacts()
|
|
}
|
|
|
|
/// Creates a new contact manually
|
|
func createContact(
|
|
name: String,
|
|
role: String = "",
|
|
company: String = "",
|
|
email: String = "",
|
|
phone: String = "",
|
|
notes: String = "",
|
|
tags: String = "",
|
|
followUpDate: Date? = nil,
|
|
contactFields: [ContactField] = [],
|
|
photoData: Data? = nil
|
|
) {
|
|
let contact = Contact(
|
|
name: name,
|
|
role: role,
|
|
company: company,
|
|
cardLabel: "Manual",
|
|
notes: notes,
|
|
tags: tags,
|
|
followUpDate: followUpDate,
|
|
email: email,
|
|
phone: phone,
|
|
photoData: photoData
|
|
)
|
|
modelContext.insert(contact)
|
|
|
|
// Add contact fields if provided
|
|
if !contactFields.isEmpty {
|
|
for field in contactFields {
|
|
field.contact = contact
|
|
modelContext.insert(field)
|
|
}
|
|
contact.contactFields = contactFields
|
|
}
|
|
|
|
saveContext()
|
|
fetchContacts()
|
|
}
|
|
|
|
/// Updates a contact's notes
|
|
func updateNotes(for contact: Contact, notes: String) {
|
|
contact.notes = notes
|
|
saveContext()
|
|
}
|
|
|
|
/// Updates a contact's tags
|
|
func updateTags(for contact: Contact, tags: String) {
|
|
contact.tags = tags
|
|
saveContext()
|
|
}
|
|
|
|
/// Sets or clears a follow-up reminder
|
|
func setFollowUp(for contact: Contact, date: Date?) {
|
|
contact.followUpDate = date
|
|
saveContext()
|
|
}
|
|
|
|
func deleteContact(_ contact: Contact) {
|
|
modelContext.delete(contact)
|
|
saveContext()
|
|
fetchContacts()
|
|
}
|
|
|
|
func relativeShareDate(for contact: Contact) -> String {
|
|
contact.lastSharedDate.formatted(
|
|
.relative(presentation: .named, unitsStyle: .abbreviated)
|
|
)
|
|
}
|
|
|
|
private func saveContext() {
|
|
do {
|
|
try modelContext.save()
|
|
} catch {
|
|
// Handle error silently for now
|
|
}
|
|
}
|
|
}
|