feat: make server owner of tabs
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import '../utils/app_config.dart';
|
||||
import 'server_tab.dart';
|
||||
|
||||
enum CommandType {
|
||||
closeTab('close_tab');
|
||||
@@ -74,6 +75,88 @@ class RejectedEvent {
|
||||
}
|
||||
}
|
||||
|
||||
enum StockAdjustmentType {
|
||||
/// `stock += quantity`; quantity may be negative (breakage, own use).
|
||||
add('add'),
|
||||
|
||||
/// `stock = quantity`, a stock count. Starts tracking an untracked product.
|
||||
set('set');
|
||||
|
||||
final String value;
|
||||
|
||||
const StockAdjustmentType(this.value);
|
||||
|
||||
static StockAdjustmentType? fromValue(String value) {
|
||||
for (final type in StockAdjustmentType.values) {
|
||||
if (type.value == value) return type;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// A stock change made on the dashboard. The server resends it on every sync
|
||||
/// until it has received the outcome, so it must be applied only once.
|
||||
class StockAdjustment {
|
||||
final String id;
|
||||
final String productId;
|
||||
final String rawType;
|
||||
final int quantity;
|
||||
final String? note;
|
||||
final String requestedBy;
|
||||
final DateTime createdAt;
|
||||
|
||||
const StockAdjustment({
|
||||
required this.id,
|
||||
required this.productId,
|
||||
required this.rawType,
|
||||
required this.quantity,
|
||||
this.note,
|
||||
required this.requestedBy,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory StockAdjustment.fromJson(Map<String, dynamic> json) {
|
||||
return StockAdjustment(
|
||||
id: json['id'] as String,
|
||||
productId: json['productId'] as String,
|
||||
rawType: json['type'] as String,
|
||||
quantity: json['quantity'] as int,
|
||||
note: json['note'] as String?,
|
||||
requestedBy: json['requestedBy'] as String? ?? '',
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
/// Null for a type this app version doesn't know; such an adjustment is
|
||||
/// neither applied nor reported.
|
||||
StockAdjustmentType? get type => StockAdjustmentType.fromValue(rawType);
|
||||
}
|
||||
|
||||
/// The server's stock of one product, after applying the events it
|
||||
/// acknowledged. The server owns the stock once it sends these.
|
||||
class ServerStock {
|
||||
final String productId;
|
||||
|
||||
/// Null when the stock of this product is not tracked. May be negative.
|
||||
final int? stock;
|
||||
|
||||
/// When the stock was last counted (set). Line events that happened before
|
||||
/// it are already in the count and don't change the stock.
|
||||
final DateTime? countedAt;
|
||||
|
||||
const ServerStock({required this.productId, this.stock, this.countedAt});
|
||||
|
||||
factory ServerStock.fromJson(Map<String, dynamic> json) {
|
||||
final countedAt = json['countedAt'] as String?;
|
||||
|
||||
return ServerStock(
|
||||
productId: json['productId'] as String,
|
||||
stock: json['stock'] as int?,
|
||||
countedAt: countedAt == null ? null : DateTime.parse(countedAt),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SyncResponse {
|
||||
final List<RemoteCommand> commands;
|
||||
final String? cursor;
|
||||
@@ -81,12 +164,38 @@ class SyncResponse {
|
||||
final List<String> acknowledgedEventIds;
|
||||
final List<RejectedEvent> rejectedEvents;
|
||||
|
||||
/// Oldest first.
|
||||
final List<StockAdjustment> stockAdjustments;
|
||||
final List<String> acknowledgedStockAdjustmentResultIds;
|
||||
final List<RejectedEvent> rejectedStockAdjustmentResults;
|
||||
|
||||
/// The venue's open tabs after applying the acknowledged events: the
|
||||
/// source of truth. Null from a server that doesn't hold the tabs yet.
|
||||
final List<ServerTab>? tabs;
|
||||
|
||||
/// Settlements made anywhere in the venue since [settlementsCursor] was
|
||||
/// last sent, oldest first.
|
||||
final List<ServerSettlement> settlements;
|
||||
final String? settlementsCursor;
|
||||
|
||||
/// The stock of the venue's products: the source of truth. Null from a
|
||||
/// server that doesn't own the stock yet; the tablet then counts its own
|
||||
/// sales and applies [stockAdjustments] itself.
|
||||
final List<ServerStock>? stock;
|
||||
|
||||
const SyncResponse({
|
||||
required this.commands,
|
||||
this.cursor,
|
||||
this.plannedTabNames = const [],
|
||||
this.acknowledgedEventIds = const [],
|
||||
this.rejectedEvents = const [],
|
||||
this.stockAdjustments = const [],
|
||||
this.acknowledgedStockAdjustmentResultIds = const [],
|
||||
this.rejectedStockAdjustmentResults = const [],
|
||||
this.tabs,
|
||||
this.settlements = const [],
|
||||
this.settlementsCursor,
|
||||
this.stock,
|
||||
});
|
||||
|
||||
factory SyncResponse.fromJson(Map<String, dynamic> json) {
|
||||
@@ -108,8 +217,61 @@ class SyncResponse {
|
||||
for (final event in json['rejectedEvents'] as List<dynamic>? ?? [])
|
||||
RejectedEvent.fromJson(event as Map<String, dynamic>),
|
||||
],
|
||||
stockAdjustments: [
|
||||
for (final adjustment
|
||||
in json['stockAdjustments'] as List<dynamic>? ?? [])
|
||||
?_parseStockAdjustment(adjustment),
|
||||
],
|
||||
acknowledgedStockAdjustmentResultIds: [
|
||||
for (final id
|
||||
in json['acknowledgedStockAdjustmentResultIds'] as List<dynamic>? ??
|
||||
[])
|
||||
id as String,
|
||||
],
|
||||
rejectedStockAdjustmentResults: [
|
||||
for (final result
|
||||
in json['rejectedStockAdjustmentResults'] as List<dynamic>? ?? [])
|
||||
RejectedEvent.fromJson(result as Map<String, dynamic>),
|
||||
],
|
||||
tabs: switch (json['tabs']) {
|
||||
final List<dynamic> tabs => [
|
||||
for (final tab in tabs)
|
||||
ServerTab.fromJson(tab as Map<String, dynamic>),
|
||||
],
|
||||
_ => null,
|
||||
},
|
||||
settlements: [
|
||||
for (final settlement in json['settlements'] as List<dynamic>? ?? [])
|
||||
ServerSettlement.fromJson(settlement as Map<String, dynamic>),
|
||||
],
|
||||
settlementsCursor: json['settlementsCursor'] as String?,
|
||||
stock: switch (json['stock']) {
|
||||
final List<dynamic> stock => [
|
||||
for (final entry in stock) ?_parseServerStock(entry),
|
||||
],
|
||||
_ => null,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// A malformed entry only leaves that product's local stock as it is.
|
||||
static ServerStock? _parseServerStock(Object? json) {
|
||||
try {
|
||||
return ServerStock.fromJson(json! as Map<String, dynamic>);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// A malformed adjustment is skipped instead of failing every sync from
|
||||
/// now on, since the server keeps sending it.
|
||||
static StockAdjustment? _parseStockAdjustment(Object? json) {
|
||||
try {
|
||||
return StockAdjustment.fromJson(json! as Map<String, dynamic>);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One line on a tab as shown in the dashboard, e.g. "10× Cola".
|
||||
@@ -166,11 +328,24 @@ class SyncRequest {
|
||||
/// Outbox tab events, oldest first, as their stored JSON objects.
|
||||
final List<Map<String, dynamic>> events;
|
||||
|
||||
/// The complete product catalog with stock: a product left out is removed
|
||||
/// from the dashboard.
|
||||
final List<Map<String, dynamic>> products;
|
||||
|
||||
/// Unacknowledged stock adjustment outcomes, oldest first.
|
||||
final List<Map<String, dynamic>> stockAdjustmentResults;
|
||||
|
||||
/// Where the settlement feed left off; null asks for the recent ones.
|
||||
final String? settlementsCursor;
|
||||
|
||||
const SyncRequest({
|
||||
required this.deviceId,
|
||||
this.cursor,
|
||||
required this.openTabs,
|
||||
this.events = const [],
|
||||
this.products = const [],
|
||||
this.stockAdjustmentResults = const [],
|
||||
this.settlementsCursor,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
@@ -178,6 +353,9 @@ class SyncRequest {
|
||||
if (cursor != null) 'cursor': cursor,
|
||||
'openTabs': openTabs.map((tab) => tab.toJson()).toList(),
|
||||
'events': events,
|
||||
'products': products,
|
||||
'stockAdjustmentResults': stockAdjustmentResults,
|
||||
if (settlementsCursor != null) 'settlementsCursor': settlementsCursor,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user