@@ -6,16 +6,20 @@ import protocol Yosemite.POSObservableDataSourceProtocol
66import struct Yosemite. POSVariableParentProduct
77import class Yosemite. GRDBObservableDataSource
88import protocol Storage. GRDBManagerProtocol
9+ import protocol Yosemite. POSCatalogSyncCoordinatorProtocol
10+ import enum Yosemite. POSCatalogSyncError
911
1012/// Controller that wraps an observable data source for POS items
1113/// Uses computed state based on data source observations for automatic UI updates
1214@Observable
1315final class PointOfSaleObservableItemsController : PointOfSaleItemsControllerProtocol {
1416 private let dataSource : POSObservableDataSourceProtocol
17+ private let catalogSyncCoordinator : POSCatalogSyncCoordinatorProtocol
18+ private let siteID : Int64
1519
16- // Track which items have been loaded at least once
17- private var hasLoadedProducts = false
18- private var hasLoadedVariationsForCurrentParent = false
20+ // Track loading and refresh for products and variations
21+ private var loadingState : LoadingState = LoadingState ( )
22+ private var refreshState : RefreshState = . idle
1923
2024 // Track current parent for variation state mapping
2125 private var currentParentItem : POSItem ?
@@ -32,51 +36,71 @@ final class PointOfSaleObservableItemsController: PointOfSaleItemsControllerProt
3236
3337 init ( siteID: Int64 ,
3438 grdbManager: GRDBManagerProtocol ,
35- currencySettings: CurrencySettings ) {
39+ currencySettings: CurrencySettings ,
40+ catalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol ) {
41+ self . siteID = siteID
3642 self . dataSource = GRDBObservableDataSource (
3743 siteID: siteID,
3844 grdbManager: grdbManager,
3945 currencySettings: currencySettings
4046 )
47+ self . catalogSyncCoordinator = catalogSyncCoordinator
4148 }
4249
4350 // periphery:ignore - used by tests
44- init ( dataSource: POSObservableDataSourceProtocol ) {
51+ init ( siteID: Int64 ,
52+ dataSource: POSObservableDataSourceProtocol ,
53+ catalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol ) {
54+ self . siteID = siteID
4555 self . dataSource = dataSource
56+ self . catalogSyncCoordinator = catalogSyncCoordinator
4657 }
4758
4859 func loadItems( base: ItemListBaseItem ) async {
4960 switch base {
5061 case . root:
62+ if shouldRefresh ( for: base) {
63+ await refreshItems ( base: base)
64+ }
5165 dataSource. loadProducts ( )
52- hasLoadedProducts = true
66+ loadingState. productsLoaded = true
67+
5368 case . parent( let parent) :
5469 guard case . variableParentProduct( let parentProduct) = parent else {
5570 assertionFailure ( " Unsupported parent type for loading items: \( parent) " )
5671 return
5772 }
5873
59- // If switching to a different parent, reset the loaded flag
6074 if currentParentItem != parent {
6175 currentParentItem = parent
62- hasLoadedVariationsForCurrentParent = false
76+ loadingState . variationsLoaded = false
6377 }
6478
79+ if shouldRefresh ( for: base) {
80+ await refreshItems ( base: base)
81+ }
6582 dataSource. loadVariations ( for: parentProduct)
66- hasLoadedVariationsForCurrentParent = true
83+ loadingState . variationsLoaded = true
6784 }
6885 }
6986
7087 func refreshItems( base: ItemListBaseItem ) async {
71- switch base {
72- case . root:
73- dataSource. refresh ( )
74- case . parent( let parent) :
75- guard case . variableParentProduct( let parentProduct) = parent else {
76- assertionFailure ( " Unsupported parent type for refreshing items: \( parent) " )
77- return
88+ if itemsEmpty ( for: base) {
89+ refreshState = . loading
90+ }
91+
92+ do {
93+ try await catalogSyncCoordinator. performIncrementalSync ( for: siteID)
94+ refreshState = . idle
95+ } catch let error as POSCatalogSyncError {
96+ switch error {
97+ case . syncAlreadyInProgress, . requestCancelled:
98+ refreshState = . idle
99+ default :
100+ refreshState = . error( error)
78101 }
79- dataSource. loadVariations ( for: parentProduct)
102+ } catch {
103+ refreshState = . error( error)
80104 }
81105 }
82106
@@ -94,67 +118,129 @@ final class PointOfSaleObservableItemsController: PointOfSaleItemsControllerProt
94118private extension PointOfSaleObservableItemsController {
95119 var containerState : ItemsContainerState {
96120 // Use .loading during initial load, .content otherwise
97- if !hasLoadedProducts && dataSource. isLoadingProducts {
121+ if !loadingState . productsLoaded && dataSource. isLoadingProducts {
98122 return . loading
99123 }
100124 return . content
101125 }
102126
103127 var rootState : ItemListState {
104- let items = dataSource. productItems
128+ computeItemListState (
129+ items: dataSource. productItems,
130+ hasLoaded: loadingState. productsLoaded,
131+ isLoading: dataSource. isLoadingProducts,
132+ error: dataSource. productError,
133+ hasMoreItems: dataSource. hasMoreProducts,
134+ errorType: PointOfSaleErrorState . errorOnLoadingProducts
135+ )
136+ }
105137
106- // Initial state - not yet loaded
107- if !hasLoadedProducts {
108- return . initial
138+ var variationStates : [ POSItem : ItemListState ] {
139+ guard let parentItem = currentParentItem else {
140+ return [ : ]
109141 }
110142
111- // Loading state - preserve existing items
112- if dataSource. isLoadingProducts {
113- return . loading( items)
114- }
143+ let state = computeItemListState (
144+ items: dataSource. variationItems,
145+ hasLoaded: loadingState. variationsLoaded,
146+ isLoading: dataSource. isLoadingVariations,
147+ error: dataSource. variationError,
148+ hasMoreItems: dataSource. hasMoreVariations,
149+ errorType: PointOfSaleErrorState . errorOnLoadingVariations
150+ )
151+ return [ parentItem: state]
152+ }
153+ }
115154
116- // Error state
117- if let error = dataSource. productError, items. isEmpty {
118- return . error( . errorOnLoadingProducts( error: error) )
155+ private extension PointOfSaleObservableItemsController {
156+ /// Determines if a refresh should be triggered
157+ func shouldRefresh( for type: ItemListBaseItem ) -> Bool {
158+ if case . error = refreshState {
159+ return true
119160 }
161+ return itemsEmpty ( for: type)
162+ }
120163
121- // Empty state
122- if items. isEmpty {
123- return . empty
164+ /// Checks if items are loaded but empty
165+ func itemsEmpty( for type: ItemListBaseItem ) -> Bool {
166+ switch type {
167+ case . root:
168+ return loadingState. productsLoaded && dataSource. productItems. isEmpty
169+ case . parent:
170+ return loadingState. variationsLoaded && dataSource. variationItems. isEmpty
124171 }
125-
126- // Loaded state
127- return . loaded( items, hasMoreItems: dataSource. hasMoreProducts)
128172 }
129173
130- var variationStates : [ POSItem : ItemListState ] {
131- guard let parentItem = currentParentItem else {
132- return [ : ]
174+ /// Computes the item list state based on current conditions
175+ func computeItemListState(
176+ items: [ POSItem ] ,
177+ hasLoaded: Bool ,
178+ isLoading: Bool ,
179+ error: Error ? ,
180+ hasMoreItems: Bool ,
181+ errorType: ( Error ) -> PointOfSaleErrorState
182+ ) -> ItemListState {
183+ // Initial state - not yet loaded
184+ if !hasLoaded {
185+ return . initial
133186 }
134187
135- let items = dataSource. variationItems
188+ // Loading state - preserve existing items
189+ if isLoading {
190+ return . loading( items)
191+ }
136192
137- // Initial state - not yet loaded
138- if !hasLoadedVariationsForCurrentParent {
139- return [ parentItem : . initial ]
193+ // Refresh loading with empty items
194+ if refreshState == . loading && items . isEmpty {
195+ return . loading ( [ ] )
140196 }
141197
142- // Loading state - preserve existing items
143- if dataSource. isLoadingVariations {
144- return [ parentItem: . loading( items) ]
198+ // Error state for refresh
199+ if case . error( let refreshError) = refreshState {
200+ return items. isEmpty
201+ ? . error( errorType ( refreshError) )
202+ : . inlineError( items, error: errorType ( refreshError) , context: . refresh)
145203 }
146204
147- // Error state
148- if let error = dataSource . variationError , items. isEmpty {
149- return [ parentItem : . error( . errorOnLoadingVariations ( error: error ) ) ]
205+ // Error state for data source observation
206+ if let error = error , items. isEmpty {
207+ return . error( errorType ( error) )
150208 }
151209
152210 // Empty state
153211 if items. isEmpty {
154- return [ parentItem : . empty]
212+ return . empty
155213 }
156214
157215 // Loaded state
158- return [ parentItem: . loaded( items, hasMoreItems: dataSource. hasMoreVariations) ]
216+ return . loaded( items, hasMoreItems: hasMoreItems)
217+ }
218+
219+ /// Represents the state of a refresh operation
220+ enum RefreshState : Equatable {
221+ case idle
222+ case loading
223+ case error( Error )
224+
225+ static func == ( lhs: RefreshState , rhs: RefreshState ) -> Bool {
226+ switch ( lhs, rhs) {
227+ case ( . idle, . idle) :
228+ return true
229+ case ( . loading, . loading) :
230+ return true
231+ case ( . error( let lhsError as POSCatalogSyncError ) , . error( let rhsError as POSCatalogSyncError ) ) :
232+ return lhsError == rhsError
233+ case ( . error( let lhsError) , . error( let rhsError) ) :
234+ return lhsError. localizedDescription == rhsError. localizedDescription
235+ default :
236+ return false
237+ }
238+ }
239+ }
240+
241+ /// Encapsulates loading state for products and variations
242+ struct LoadingState {
243+ var productsLoaded = false
244+ var variationsLoaded = false
159245 }
160246}
0 commit comments