Christopher Hotchkiss Christopher Hotchkiss
Login

Code

remindwall / RWPureSwift / Sources / EditSettingsNew / Trackees / Trackees.swift

Back to remindwall — Current RemindWall

Download this file

import ComposableArchitecture
import Dao
import SQLiteData
import SwiftUI

@Reducer
public struct TrackeesFeature: Sendable {
    @Dependency(\.defaultDatabase) var defaultDatabase
    @Dependency(\.uuid) var uuid
    
    @ObservableState
    public struct State: Equatable {
        @Presents var destination: Destination.State?
        var path = StackState<TrackeeDetailFeature.State>()
        
        @FetchAll(Trackee.none)
        var trackees: [Trackee]
        
        public init(){
            self._trackees = FetchAll(Trackee.all.order(by: \.name))
        }
    }
    
    public enum Action {
        case addButtonTapped
        case deleteTrackee(Trackee.ID)
        case destination(PresentationAction<Destination.Action>)
        case path(StackActionOf<TrackeeDetailFeature>)
        case reloadTrackees
    }
    
    public init() {
    }
    
    public var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .addButtonTapped:
                state.destination = .addTrackee(
                    AddTrackeeFeature.State(
                        trackee: Trackee(id: Trackee.ID(uuid()), name: "")
                    )
                )
                return .none
                
            case let .deleteTrackee(trackeeId):
                return .run { [t = state.$trackees, dd = self.defaultDatabase] send in
                    await withErrorReporting {
                        try await dd.write { db in
                            try ReminderTime.where { $0.trackeeId.eq(trackeeId) }.delete().execute(db)
                            try Trackee.find(trackeeId).delete().execute(db)
                        }
                        try await t.load(Trackee.all.order(by: \.name))
                    }
                }
                
            case let .destination(.presented(.addTrackee(.delegate(.saveTrackee(trackee))))):
                return .run { [t = state.$trackees, dd = self.defaultDatabase] send in
                    await withErrorReporting {
                        try await dd.write { db in
                            try Trackee.insert{trackee}.execute(db)
                        }
                        try await t.load(Trackee.all.order(by: \.name))
                    }
                }
                
            case let .path(.element(id: id, action: .delegate(.confirmDeletion))):
              guard let detailState = state.path[id: id]
              else { return .none }
                return .send(.deleteTrackee(detailState.trackee.id))
                
            case .reloadTrackees:
                return .run { [t = state.$trackees] send in
                    _ = await withErrorReporting {
                        try await t.load(Trackee.all.order(by: \.name))
                    }
                }
            case .destination:
                return .none
            case .path:
                return .none
            }
        }.ifLet(\.$destination, action: \.destination) {
            Destination.body
        }.forEach(\.path, action: \.path) {
            TrackeeDetailFeature()
        }
    }
}

extension TrackeesFeature {
  @Reducer
  public enum Destination {
    case addTrackee(AddTrackeeFeature)
  }
}

extension TrackeesFeature.Destination.State: Equatable {}
    

public struct TrackeesView: View {
    @Bindable var store: StoreOf<TrackeesFeature>
    public let isEmbedded: Bool
    
    public init(store: StoreOf<TrackeesFeature>, isEmbedded: Bool = false) {
        self.store = store
        self.isEmbedded = isEmbedded
    }
    
    public var body: some View {
        if isEmbedded {
            embeddedContent
        } else {
            standaloneContent
        }
    }

    @ViewBuilder
    private func trackeeRow(_ trackee: Trackee) -> some View {
        HStack {
            Text(trackee.name)
            Spacer()
            statusBadge(enabled: trackee.remindersEnabled)
        }
    }

    /// A pill that names the reminder state outright — "Active" (green) vs
    /// "Paused" (orange) — so a paused trackee pops in a list of active ones
    /// without opening the detail.
    private func statusBadge(enabled: Bool) -> some View {
        let tint: Color = enabled ? .green : .orange
        return Label(
            enabled ? "Active" : "Paused",
            systemImage: enabled ? "bell.fill" : "bell.slash.fill"
        )
        .labelStyle(.titleAndIcon)
        .font(.caption.weight(.semibold))
        .foregroundStyle(tint)
        .padding(.horizontal, 8)
        .padding(.vertical, 3)
        .background(Capsule().fill(tint.opacity(0.15)))
        .accessibilityElement(children: .ignore)
        .accessibilityLabel(enabled ? "Reminders active" : "Reminders paused")
    }
    
    @ViewBuilder
    private var embeddedContent: some View {
        if store.trackees.isEmpty {
            Text("No trackees configured")
        } else {
            ForEach(store.trackees){ trackee in
                NavigationLink(state: TrackeeDetailFeature.State(trackee: trackee)) {
                    trackeeRow(trackee)
                }
            }
        }
        
        Color.clear
            .frame(height: 0)
            .listRowBackground(Color.clear)
            .listRowSeparator(.hidden)
            .sheet(
                item: $store.scope(state: \.$destination, action: \.destination).addTrackee
            ) { addTrackeeStore in
                NavigationStack {
                    AddTrackeeView(store: addTrackeeStore)
                }
            }
    }
    
    @ViewBuilder
    private var standaloneContent: some View {
        NavigationStack(path: $store.scope(state: \.path, action: \.path)) {
            List {
                if store.trackees.isEmpty {
                    Text("No trackees configured")
                } else {
                    ForEach(store.trackees){ trackee in
                        NavigationLink(state: TrackeeDetailFeature.State(trackee: trackee)) {
                            trackeeRow(trackee)
                        }
                        .buttonStyle(.borderless)
                    }
                }
            }.navigationTitle("Trackees")
                .toolbar {
                    ToolbarItem {
                        Button {
                            store.send(.addButtonTapped)
                        } label: {
                            Image(systemName: "plus")
                        }
                    }
                }
        } destination: { store in
            TrackeeDetailView(store: store)
        }
        .sheet(
            item: $store.scope(state: \.$destination, action: \.destination).addTrackee
          ) { addTrackeeStore in
            NavigationStack {
              AddTrackeeView(store: addTrackeeStore)
            }
          }
    }
}

#Preview {
    let _ = prepareDependencies {
        $0.defaultDatabase = try! $0.appDatabase()
      }
    
    TrackeesView(
    store: Store(
      initialState: TrackeesFeature.State()
    ) {
        TrackeesFeature()
    }
  )
}