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
This commit is contained in:
137
Lendair/ViewModels/NotificationsViewModel.swift
Normal file
137
Lendair/ViewModels/NotificationsViewModel.swift
Normal file
@@ -0,0 +1,137 @@
|
||||
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", {})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user