feat: add alot

This commit is contained in:
2026-10-08 04:12:36 +02:00
parent ccf5165417
commit fc607cb7bd
99 changed files with 4981 additions and 3655 deletions
+26
View File
@@ -0,0 +1,26 @@
/// User-facing failures. View models expose these and views translate them
/// with `AppLocalizations.errorText`, so a typo can't silently skip l10n.
enum AppError {
loadBar,
createTab,
outOfStock,
addProductToTab,
updateQuantity,
closeTab,
renameTab,
deleteTab,
loadHistory,
loadMoreTabs,
loadProducts,
addProduct,
updateProduct,
deleteProduct,
checkPinStatus,
savePin,
incorrectPin,
verifyPin,
currentPinIncorrect,
tooManyPinAttempts,
loadSettings,
saveSettings,
}
+4 -22
View File
@@ -1,23 +1,20 @@
import 'package:flutter/foundation.dart';
import 'tab_item.dart';
import '../utils/collection_utils.dart';
class BarTab {
final String id;
final String customerName;
final String status;
final DateTime openedAt;
final DateTime? closedAt;
final int version;
final List<TabItem> items;
const BarTab({
required this.id,
required this.customerName,
required this.status,
required this.openedAt,
this.version = 0,
required this.items,
this.closedAt,
});
int get totalInCents {
@@ -28,32 +25,17 @@ class BarTab {
return items.fold<int>(0, (total, item) => total + item.quantity);
}
String get formattedTotal {
return '€${(totalInCents / 100).toStringAsFixed(2)}';
}
bool get isOpen => status == 'open';
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BarTab &&
id == other.id &&
customerName == other.customerName &&
status == other.status &&
openedAt == other.openedAt &&
closedAt == other.closedAt &&
version == other.version &&
listEquals(items, other.items);
@override
int get hashCode => Object.hash(
id,
customerName,
status,
openedAt,
closedAt,
version,
Object.hashAll(items),
);
int get hashCode =>
Object.hash(id, customerName, openedAt, version, Object.hashAll(items));
}
+2 -8
View File
@@ -1,8 +1,7 @@
import 'package:intl/intl.dart';
import 'package:flutter/foundation.dart';
import 'closed_tab_item.dart';
import 'payment_method.dart';
import '../utils/collection_utils.dart';
class ClosedTab {
final String id;
@@ -10,8 +9,8 @@ class ClosedTab {
final String customerName;
final DateTime closedAt;
final PaymentMethod paymentMethod;
final List<ClosedTabItem> items;
const ClosedTab({
required this.id,
required this.originalTabId,
@@ -27,11 +26,6 @@ class ClosedTab {
int get totalInCents =>
items.fold(0, (sum, item) => sum + item.lineTotalInCents);
String get formattedTotal =>
NumberFormat.simpleCurrency().format(totalInCents / 100);
String get formattedPaymentMethod => paymentMethod.label;
@override
bool operator ==(Object other) =>
identical(this, other) ||
+8 -9
View File
@@ -1,16 +1,15 @@
enum PaymentMethod {
cash('cash', 'Cash', 'Cash payment'),
payconiq('payconiq', 'Payconiq', 'Payconiq QR payment'),
paymentPending('payment_pending', 'Payment pending', 'Unpaid remote close');
cash('cash'),
payconiq('payconiq'),
paymentPending('payment_pending');
final String value;
final String label;
final String description;
const PaymentMethod(this.value, this.label, this.description);
const PaymentMethod(this.value);
static PaymentMethod fromValue(String value) => PaymentMethod.values.firstWhere(
(e) => e.value == value,
static PaymentMethod fromValue(String value) =>
PaymentMethod.values.firstWhere(
(method) => method.value == value,
orElse: () => PaymentMethod.cash,
);
}
}
+9 -15
View File
@@ -21,12 +21,6 @@ class Product {
bool get isLowStock => stockQuantity <= lowStockThreshold;
String get formattedPrice {
final euros = priceInCents / 100;
return '€${euros.toStringAsFixed(2)}';
}
Product copyWith({
String? id,
String? name,
@@ -64,13 +58,13 @@ class Product {
@override
int get hashCode => Object.hash(
id,
name,
category,
stockQuantity,
lowStockThreshold,
priceInCents,
imagePath,
active,
);
id,
name,
category,
stockQuantity,
lowStockThreshold,
priceInCents,
imagePath,
active,
);
}
+54 -30
View File
@@ -1,6 +1,25 @@
import '../utils/app_config.dart';
enum CommandType {
closeTab('close_tab');
final String value;
const CommandType(this.value);
static CommandType? fromValue(String value) {
for (final type in CommandType.values) {
if (type.value == value) return type;
}
return null;
}
}
enum CommandResult { completed, conflict, failed, expired }
class RemoteCommand {
final String id;
final String type;
final String rawType;
final String tabId;
final int expectedVersion;
final String paymentMethod;
@@ -9,7 +28,7 @@ class RemoteCommand {
const RemoteCommand({
required this.id,
required this.type,
required this.rawType,
required this.tabId,
required this.expectedVersion,
required this.paymentMethod,
@@ -20,7 +39,7 @@ class RemoteCommand {
factory RemoteCommand.fromJson(Map<String, dynamic> json) {
return RemoteCommand(
id: json['id'] as String,
type: json['type'] as String,
rawType: json['type'] as String,
tabId: json['tabId'] as String,
expectedVersion: json['expectedVersion'] as int,
paymentMethod: json['paymentMethod'] as String? ?? 'cash',
@@ -29,8 +48,13 @@ class RemoteCommand {
);
}
/// Null when the server sends a command this app version doesn't know.
CommandType? get type => CommandType.fromValue(rawType);
bool get isExpired {
final cutoff = DateTime.now().subtract(const Duration(hours: 24));
final cutoff = DateTime.now().subtract(
const Duration(hours: kCommandExpiryHours),
);
return createdAt.isBefore(cutoff);
}
}
@@ -40,24 +64,23 @@ class SyncResponse {
final String? cursor;
final List<String> plannedTabNames;
SyncResponse({
const SyncResponse({
required this.commands,
this.cursor,
this.plannedTabNames = const [],
});
factory SyncResponse.fromJson(Map<String, dynamic> json) {
final commandsList = (json['commands'] as List<dynamic>?)
?.map((e) => RemoteCommand.fromJson(e as Map<String, dynamic>))
.toList() ??
[];
return SyncResponse(
commands: commandsList,
commands: [
for (final command in json['commands'] as List<dynamic>? ?? [])
RemoteCommand.fromJson(command as Map<String, dynamic>),
],
cursor: json['cursor'] as String?,
plannedTabNames: (json['plannedTabNames'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
[],
plannedTabNames: [
for (final name in json['plannedTabNames'] as List<dynamic>? ?? [])
name as String,
],
);
}
}
@@ -78,12 +101,12 @@ class SnapshotTab {
});
Map<String, dynamic> toJson() => {
'id': id,
'customerName': customerName,
'version': version,
'totalInCents': totalInCents,
'itemCount': itemCount,
};
'id': id,
'customerName': customerName,
'version': version,
'totalInCents': totalInCents,
'itemCount': itemCount,
};
}
class SyncRequest {
@@ -98,15 +121,15 @@ class SyncRequest {
});
Map<String, dynamic> toJson() => {
'deviceId': deviceId,
if (cursor != null) 'cursor': cursor,
'openTabs': openTabs.map((t) => t.toJson()).toList(),
};
'deviceId': deviceId,
if (cursor != null) 'cursor': cursor,
'openTabs': openTabs.map((tab) => tab.toJson()).toList(),
};
}
class CommandAck {
final String commandId;
final String status;
final CommandResult status;
final String? result;
const CommandAck({
@@ -116,10 +139,10 @@ class CommandAck {
});
Map<String, dynamic> toJson() => {
'commandId': commandId,
'status': status,
if (result != null) 'result': result,
};
'commandId': commandId,
'status': status.name,
if (result != null) 'result': result,
};
}
enum RemoteSyncEventType {
@@ -127,6 +150,7 @@ enum RemoteSyncEventType {
syncCompleted,
syncFailed,
conflict,
unauthorized,
}
class RemoteSyncEvent {
@@ -141,4 +165,4 @@ class RemoteSyncEvent {
this.requestedBy,
this.message,
});
}
}
+2 -9
View File
@@ -1,5 +1,6 @@
import 'package:flutter/foundation.dart';
import 'tab_item_purchase.dart';
import '../utils/collection_utils.dart';
class TabItem {
final String id;
@@ -22,14 +23,6 @@ class TabItem {
int get lineTotalInCents => quantity * unitPriceInCents;
String get formattedLineTotal {
return '€${(lineTotalInCents / 100).toStringAsFixed(2)}';
}
String get formattedUnitPrice {
return '€${(unitPriceInCents / 100).toStringAsFixed(2)}';
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
+3 -5
View File
@@ -4,8 +4,6 @@ class Venue {
const Venue({required this.id, required this.name});
factory Venue.fromJson(Map<String, dynamic> json) => Venue(
id: json['id'] as String,
name: json['name'] as String,
);
}
factory Venue.fromJson(Map<String, dynamic> json) =>
Venue(id: json['id'] as String, name: json['name'] as String);
}