Files
FrenoCorp/Lendair/ViewModels/NotificationsViewModel.swift
Michael Freno 4f1ff9dbb0 feat: Implement NotificationsView component for Lendair iOS
- Create NotificationsView.swift with SwiftUI List and pull-to-refresh
- Create NotificationRowView.swift for individual notification items
- Create NotificationsViewModel.swift with MVVM pattern
- Implement empty state view for no notifications
- Add mark-as-read and mark-all-as-read functionality
- Support notification types: loan approved/rejected, payment received/due, new lender, system updates
- Add toolbar action for marking all notifications as read
- Include README.md with architecture documentation and integration guide

Next: Connect tRPC notifications router for data fetching
2026-05-03 12:11:00 -04:00

138 lines
3.9 KiB
Swift

import Foundation
import SwiftUI
@MainActor
class NotificationsViewModel: ObservableObject {
@Published var notifications: [NotificationItem] = []
@Published var isLoading: Bool = false
@Published var lastRefreshDate: Date?
private let notificationsService: NotificationsService
init(notificationsService: NotificationsService = NotificationsService()) {
self.notificationsService = notificationsService
}
func fetchNotifications() async {
isLoading = true
defer {
isLoading = false
lastRefreshDate = Date()
}
do {
let fetchedNotifications = try await notificationsService.list()
notifications = fetchedNotifications.sorted { $0.createdAt > $1.createdAt }
} catch {
print("Failed to fetch notifications: \(error)")
}
}
func refresh() async {
await fetchNotifications()
}
func markAsRead(id: String) async {
guard let index = notifications.firstIndex(where: { $0.id == id }) else { return }
do {
try await notificationsService.markAsRead(id: id)
notifications[index].isRead = true
} catch {
print("Failed to mark notification as read: \(error)")
}
}
func markAllAsRead() async {
let unreadIds = notifications.filter { !$0.isRead }.map { $0.id }
guard !unreadIds.isEmpty else { return }
do {
try await notificationsService.markAllAsRead()
for index in notifications.indices {
notifications[index].isRead = true
}
} catch {
print("Failed to mark all as read: \(error)")
}
}
var unreadCount: Int {
notifications.filter { !$0.isRead }.count
}
}
struct NotificationItem: Identifiable, Equatable {
let id: String
let type: NotificationType
let title: String
let message: String
let createdAt: Date
var isRead: Bool
static func == (lhs: NotificationItem, rhs: NotificationItem) -> Bool {
lhs.id == rhs.id && lhs.isRead == rhs.isRead
}
}
enum NotificationType: String, CaseIterable {
case loanApproved = "LOAN_APPROVED"
case loanRejected = "LOAN_REJECTED"
case paymentReceived = "PAYMENT_RECEIVED"
case paymentDue = "PAYMENT_DUE"
case newLender = "NEW_LENDER"
case systemUpdate = "SYSTEM_UPDATE"
var icon: String {
switch self {
case .loanApproved:
return "checkmark.circle.fill"
case .loanRejected:
return "xmark.circle.fill"
case .paymentReceived:
return "arrow.down.circle.fill"
case .paymentDue:
return "exclamationmark.circle.fill"
case .newLender:
return "person.circle.fill"
case .systemUpdate:
return "info.circle.fill"
}
}
var color: Color {
switch self {
case .loanApproved:
return .green
case .loanRejected:
return .red
case .paymentReceived:
return .green
case .paymentDue:
return .orange
case .newLender:
return .blue
case .systemUpdate:
return .gray
}
}
}
class NotificationsService {
func list() async throws -> [NotificationItem] {
// TODO: Connect to tRPC notifications router
// let result = await APIClient.shared.query("notifications.list")
return []
}
func markAsRead(id: String) async throws {
// TODO: Connect to tRPC notifications router
// try await APIClient.shared.mutation("notifications.markAsRead", { id })
}
func markAllAsRead() async throws {
// TODO: Connect to tRPC notifications router
// try await APIClient.shared.mutation("notifications.markAllAsRead", {})
}
}