feat: add remote products and qr code for payconic
This commit is contained in:
@@ -36,8 +36,13 @@ class Products extends Table {
|
||||
|
||||
IntColumn get priceInCents => integer()();
|
||||
|
||||
/// The picture shown on the tile: a local file, also offline.
|
||||
TextColumn get imagePath => text().nullable()();
|
||||
|
||||
/// The server's picture of this product (`/images/<imageId>`); [imagePath]
|
||||
/// is its downloaded copy once that finished. Null: none on the server.
|
||||
TextColumn get imageId => text().nullable()();
|
||||
|
||||
BoolColumn get active => boolean().withDefault(const Constant(true))();
|
||||
|
||||
@override
|
||||
@@ -227,6 +232,22 @@ class StockAdjustmentResults extends Table {
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
/// Pictures taken on this tablet, waiting to be uploaded. Once uploaded, the
|
||||
/// product points at the server's copy and the row is removed; offline it
|
||||
/// waits here, while the tile already shows the local file.
|
||||
@DataClassName('PendingImageUploadRow')
|
||||
class PendingImageUploads extends Table {
|
||||
/// One picture per product: a newer one replaces a waiting one.
|
||||
TextColumn get productId => text()();
|
||||
|
||||
TextColumn get localPath => text()();
|
||||
|
||||
DateTimeColumn get createdAt => dateTime()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {productId};
|
||||
}
|
||||
|
||||
@DriftDatabase(
|
||||
tables: [
|
||||
Products,
|
||||
@@ -240,13 +261,14 @@ class StockAdjustmentResults extends Table {
|
||||
ProcessedRemoteCommands,
|
||||
TabEvents,
|
||||
StockAdjustmentResults,
|
||||
PendingImageUploads,
|
||||
],
|
||||
)
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 17;
|
||||
int get schemaVersion => 18;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration {
|
||||
@@ -362,6 +384,13 @@ class AppDatabase extends _$AppDatabase {
|
||||
remoteSyncState.settlementsCursor,
|
||||
);
|
||||
}
|
||||
|
||||
if (from < 18) {
|
||||
if (!await hasTableColumn('products', 'image_id')) {
|
||||
await migrator.addColumn(products, products.imageId);
|
||||
}
|
||||
await migrator.createTable(pendingImageUploads);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -97,6 +97,17 @@ class $ProductsTable extends Products
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
static const VerificationMeta _imageIdMeta = const VerificationMeta(
|
||||
'imageId',
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumn<String> imageId = GeneratedColumn<String>(
|
||||
'image_id',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
static const VerificationMeta _activeMeta = const VerificationMeta('active');
|
||||
@override
|
||||
late final GeneratedColumn<bool> active = GeneratedColumn<bool>(
|
||||
@@ -120,6 +131,7 @@ class $ProductsTable extends Products
|
||||
infiniteStock,
|
||||
priceInCents,
|
||||
imagePath,
|
||||
imageId,
|
||||
active,
|
||||
];
|
||||
@override
|
||||
@@ -203,6 +215,12 @@ class $ProductsTable extends Products
|
||||
imagePath.isAcceptableOrUnknown(data['image_path']!, _imagePathMeta),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('image_id')) {
|
||||
context.handle(
|
||||
_imageIdMeta,
|
||||
imageId.isAcceptableOrUnknown(data['image_id']!, _imageIdMeta),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('active')) {
|
||||
context.handle(
|
||||
_activeMeta,
|
||||
@@ -250,6 +268,10 @@ class $ProductsTable extends Products
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}image_path'],
|
||||
),
|
||||
imageId: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}image_id'],
|
||||
),
|
||||
active: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.bool,
|
||||
data['${effectivePrefix}active'],
|
||||
@@ -273,7 +295,13 @@ class ProductRow extends DataClass implements Insertable<ProductRow> {
|
||||
/// Products like tap water that are never counted; their stock is ignored.
|
||||
final bool infiniteStock;
|
||||
final int priceInCents;
|
||||
|
||||
/// The picture shown on the tile: a local file, also offline.
|
||||
final String? imagePath;
|
||||
|
||||
/// The server's picture of this product (`/images/<imageId>`); [imagePath]
|
||||
/// is its downloaded copy once that finished. Null: none on the server.
|
||||
final String? imageId;
|
||||
final bool active;
|
||||
const ProductRow({
|
||||
required this.id,
|
||||
@@ -284,6 +312,7 @@ class ProductRow extends DataClass implements Insertable<ProductRow> {
|
||||
required this.infiniteStock,
|
||||
required this.priceInCents,
|
||||
this.imagePath,
|
||||
this.imageId,
|
||||
required this.active,
|
||||
});
|
||||
@override
|
||||
@@ -299,6 +328,9 @@ class ProductRow extends DataClass implements Insertable<ProductRow> {
|
||||
if (!nullToAbsent || imagePath != null) {
|
||||
map['image_path'] = Variable<String>(imagePath);
|
||||
}
|
||||
if (!nullToAbsent || imageId != null) {
|
||||
map['image_id'] = Variable<String>(imageId);
|
||||
}
|
||||
map['active'] = Variable<bool>(active);
|
||||
return map;
|
||||
}
|
||||
@@ -315,6 +347,9 @@ class ProductRow extends DataClass implements Insertable<ProductRow> {
|
||||
imagePath: imagePath == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(imagePath),
|
||||
imageId: imageId == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(imageId),
|
||||
active: Value(active),
|
||||
);
|
||||
}
|
||||
@@ -333,6 +368,7 @@ class ProductRow extends DataClass implements Insertable<ProductRow> {
|
||||
infiniteStock: serializer.fromJson<bool>(json['infiniteStock']),
|
||||
priceInCents: serializer.fromJson<int>(json['priceInCents']),
|
||||
imagePath: serializer.fromJson<String?>(json['imagePath']),
|
||||
imageId: serializer.fromJson<String?>(json['imageId']),
|
||||
active: serializer.fromJson<bool>(json['active']),
|
||||
);
|
||||
}
|
||||
@@ -348,6 +384,7 @@ class ProductRow extends DataClass implements Insertable<ProductRow> {
|
||||
'infiniteStock': serializer.toJson<bool>(infiniteStock),
|
||||
'priceInCents': serializer.toJson<int>(priceInCents),
|
||||
'imagePath': serializer.toJson<String?>(imagePath),
|
||||
'imageId': serializer.toJson<String?>(imageId),
|
||||
'active': serializer.toJson<bool>(active),
|
||||
};
|
||||
}
|
||||
@@ -361,6 +398,7 @@ class ProductRow extends DataClass implements Insertable<ProductRow> {
|
||||
bool? infiniteStock,
|
||||
int? priceInCents,
|
||||
Value<String?> imagePath = const Value.absent(),
|
||||
Value<String?> imageId = const Value.absent(),
|
||||
bool? active,
|
||||
}) => ProductRow(
|
||||
id: id ?? this.id,
|
||||
@@ -371,6 +409,7 @@ class ProductRow extends DataClass implements Insertable<ProductRow> {
|
||||
infiniteStock: infiniteStock ?? this.infiniteStock,
|
||||
priceInCents: priceInCents ?? this.priceInCents,
|
||||
imagePath: imagePath.present ? imagePath.value : this.imagePath,
|
||||
imageId: imageId.present ? imageId.value : this.imageId,
|
||||
active: active ?? this.active,
|
||||
);
|
||||
ProductRow copyWithCompanion(ProductsCompanion data) {
|
||||
@@ -391,6 +430,7 @@ class ProductRow extends DataClass implements Insertable<ProductRow> {
|
||||
? data.priceInCents.value
|
||||
: this.priceInCents,
|
||||
imagePath: data.imagePath.present ? data.imagePath.value : this.imagePath,
|
||||
imageId: data.imageId.present ? data.imageId.value : this.imageId,
|
||||
active: data.active.present ? data.active.value : this.active,
|
||||
);
|
||||
}
|
||||
@@ -406,6 +446,7 @@ class ProductRow extends DataClass implements Insertable<ProductRow> {
|
||||
..write('infiniteStock: $infiniteStock, ')
|
||||
..write('priceInCents: $priceInCents, ')
|
||||
..write('imagePath: $imagePath, ')
|
||||
..write('imageId: $imageId, ')
|
||||
..write('active: $active')
|
||||
..write(')'))
|
||||
.toString();
|
||||
@@ -421,6 +462,7 @@ class ProductRow extends DataClass implements Insertable<ProductRow> {
|
||||
infiniteStock,
|
||||
priceInCents,
|
||||
imagePath,
|
||||
imageId,
|
||||
active,
|
||||
);
|
||||
@override
|
||||
@@ -435,6 +477,7 @@ class ProductRow extends DataClass implements Insertable<ProductRow> {
|
||||
other.infiniteStock == this.infiniteStock &&
|
||||
other.priceInCents == this.priceInCents &&
|
||||
other.imagePath == this.imagePath &&
|
||||
other.imageId == this.imageId &&
|
||||
other.active == this.active);
|
||||
}
|
||||
|
||||
@@ -447,6 +490,7 @@ class ProductsCompanion extends UpdateCompanion<ProductRow> {
|
||||
final Value<bool> infiniteStock;
|
||||
final Value<int> priceInCents;
|
||||
final Value<String?> imagePath;
|
||||
final Value<String?> imageId;
|
||||
final Value<bool> active;
|
||||
final Value<int> rowid;
|
||||
const ProductsCompanion({
|
||||
@@ -458,6 +502,7 @@ class ProductsCompanion extends UpdateCompanion<ProductRow> {
|
||||
this.infiniteStock = const Value.absent(),
|
||||
this.priceInCents = const Value.absent(),
|
||||
this.imagePath = const Value.absent(),
|
||||
this.imageId = const Value.absent(),
|
||||
this.active = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
});
|
||||
@@ -470,6 +515,7 @@ class ProductsCompanion extends UpdateCompanion<ProductRow> {
|
||||
this.infiniteStock = const Value.absent(),
|
||||
required int priceInCents,
|
||||
this.imagePath = const Value.absent(),
|
||||
this.imageId = const Value.absent(),
|
||||
this.active = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
}) : id = Value(id),
|
||||
@@ -487,6 +533,7 @@ class ProductsCompanion extends UpdateCompanion<ProductRow> {
|
||||
Expression<bool>? infiniteStock,
|
||||
Expression<int>? priceInCents,
|
||||
Expression<String>? imagePath,
|
||||
Expression<String>? imageId,
|
||||
Expression<bool>? active,
|
||||
Expression<int>? rowid,
|
||||
}) {
|
||||
@@ -499,6 +546,7 @@ class ProductsCompanion extends UpdateCompanion<ProductRow> {
|
||||
if (infiniteStock != null) 'infinite_stock': infiniteStock,
|
||||
if (priceInCents != null) 'price_in_cents': priceInCents,
|
||||
if (imagePath != null) 'image_path': imagePath,
|
||||
if (imageId != null) 'image_id': imageId,
|
||||
if (active != null) 'active': active,
|
||||
if (rowid != null) 'rowid': rowid,
|
||||
});
|
||||
@@ -513,6 +561,7 @@ class ProductsCompanion extends UpdateCompanion<ProductRow> {
|
||||
Value<bool>? infiniteStock,
|
||||
Value<int>? priceInCents,
|
||||
Value<String?>? imagePath,
|
||||
Value<String?>? imageId,
|
||||
Value<bool>? active,
|
||||
Value<int>? rowid,
|
||||
}) {
|
||||
@@ -525,6 +574,7 @@ class ProductsCompanion extends UpdateCompanion<ProductRow> {
|
||||
infiniteStock: infiniteStock ?? this.infiniteStock,
|
||||
priceInCents: priceInCents ?? this.priceInCents,
|
||||
imagePath: imagePath ?? this.imagePath,
|
||||
imageId: imageId ?? this.imageId,
|
||||
active: active ?? this.active,
|
||||
rowid: rowid ?? this.rowid,
|
||||
);
|
||||
@@ -557,6 +607,9 @@ class ProductsCompanion extends UpdateCompanion<ProductRow> {
|
||||
if (imagePath.present) {
|
||||
map['image_path'] = Variable<String>(imagePath.value);
|
||||
}
|
||||
if (imageId.present) {
|
||||
map['image_id'] = Variable<String>(imageId.value);
|
||||
}
|
||||
if (active.present) {
|
||||
map['active'] = Variable<bool>(active.value);
|
||||
}
|
||||
@@ -577,6 +630,7 @@ class ProductsCompanion extends UpdateCompanion<ProductRow> {
|
||||
..write('infiniteStock: $infiniteStock, ')
|
||||
..write('priceInCents: $priceInCents, ')
|
||||
..write('imagePath: $imagePath, ')
|
||||
..write('imageId: $imageId, ')
|
||||
..write('active: $active, ')
|
||||
..write('rowid: $rowid')
|
||||
..write(')'))
|
||||
@@ -4447,6 +4501,278 @@ class StockAdjustmentResultsCompanion
|
||||
}
|
||||
}
|
||||
|
||||
class $PendingImageUploadsTable extends PendingImageUploads
|
||||
with TableInfo<$PendingImageUploadsTable, PendingImageUploadRow> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$PendingImageUploadsTable(this.attachedDatabase, [this._alias]);
|
||||
static const VerificationMeta _productIdMeta = const VerificationMeta(
|
||||
'productId',
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumn<String> productId = GeneratedColumn<String>(
|
||||
'product_id',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const VerificationMeta _localPathMeta = const VerificationMeta(
|
||||
'localPath',
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumn<String> localPath = GeneratedColumn<String>(
|
||||
'local_path',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const VerificationMeta _createdAtMeta = const VerificationMeta(
|
||||
'createdAt',
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
|
||||
'created_at',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.dateTime,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [productId, localPath, createdAt];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'pending_image_uploads';
|
||||
@override
|
||||
VerificationContext validateIntegrity(
|
||||
Insertable<PendingImageUploadRow> instance, {
|
||||
bool isInserting = false,
|
||||
}) {
|
||||
final context = VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('product_id')) {
|
||||
context.handle(
|
||||
_productIdMeta,
|
||||
productId.isAcceptableOrUnknown(data['product_id']!, _productIdMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_productIdMeta);
|
||||
}
|
||||
if (data.containsKey('local_path')) {
|
||||
context.handle(
|
||||
_localPathMeta,
|
||||
localPath.isAcceptableOrUnknown(data['local_path']!, _localPathMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_localPathMeta);
|
||||
}
|
||||
if (data.containsKey('created_at')) {
|
||||
context.handle(
|
||||
_createdAtMeta,
|
||||
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_createdAtMeta);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {productId};
|
||||
@override
|
||||
PendingImageUploadRow map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return PendingImageUploadRow(
|
||||
productId: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}product_id'],
|
||||
)!,
|
||||
localPath: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}local_path'],
|
||||
)!,
|
||||
createdAt: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}created_at'],
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
$PendingImageUploadsTable createAlias(String alias) {
|
||||
return $PendingImageUploadsTable(attachedDatabase, alias);
|
||||
}
|
||||
}
|
||||
|
||||
class PendingImageUploadRow extends DataClass
|
||||
implements Insertable<PendingImageUploadRow> {
|
||||
/// One picture per product: a newer one replaces a waiting one.
|
||||
final String productId;
|
||||
final String localPath;
|
||||
final DateTime createdAt;
|
||||
const PendingImageUploadRow({
|
||||
required this.productId,
|
||||
required this.localPath,
|
||||
required this.createdAt,
|
||||
});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['product_id'] = Variable<String>(productId);
|
||||
map['local_path'] = Variable<String>(localPath);
|
||||
map['created_at'] = Variable<DateTime>(createdAt);
|
||||
return map;
|
||||
}
|
||||
|
||||
PendingImageUploadsCompanion toCompanion(bool nullToAbsent) {
|
||||
return PendingImageUploadsCompanion(
|
||||
productId: Value(productId),
|
||||
localPath: Value(localPath),
|
||||
createdAt: Value(createdAt),
|
||||
);
|
||||
}
|
||||
|
||||
factory PendingImageUploadRow.fromJson(
|
||||
Map<String, dynamic> json, {
|
||||
ValueSerializer? serializer,
|
||||
}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return PendingImageUploadRow(
|
||||
productId: serializer.fromJson<String>(json['productId']),
|
||||
localPath: serializer.fromJson<String>(json['localPath']),
|
||||
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'productId': serializer.toJson<String>(productId),
|
||||
'localPath': serializer.toJson<String>(localPath),
|
||||
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||
};
|
||||
}
|
||||
|
||||
PendingImageUploadRow copyWith({
|
||||
String? productId,
|
||||
String? localPath,
|
||||
DateTime? createdAt,
|
||||
}) => PendingImageUploadRow(
|
||||
productId: productId ?? this.productId,
|
||||
localPath: localPath ?? this.localPath,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
);
|
||||
PendingImageUploadRow copyWithCompanion(PendingImageUploadsCompanion data) {
|
||||
return PendingImageUploadRow(
|
||||
productId: data.productId.present ? data.productId.value : this.productId,
|
||||
localPath: data.localPath.present ? data.localPath.value : this.localPath,
|
||||
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('PendingImageUploadRow(')
|
||||
..write('productId: $productId, ')
|
||||
..write('localPath: $localPath, ')
|
||||
..write('createdAt: $createdAt')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(productId, localPath, createdAt);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is PendingImageUploadRow &&
|
||||
other.productId == this.productId &&
|
||||
other.localPath == this.localPath &&
|
||||
other.createdAt == this.createdAt);
|
||||
}
|
||||
|
||||
class PendingImageUploadsCompanion
|
||||
extends UpdateCompanion<PendingImageUploadRow> {
|
||||
final Value<String> productId;
|
||||
final Value<String> localPath;
|
||||
final Value<DateTime> createdAt;
|
||||
final Value<int> rowid;
|
||||
const PendingImageUploadsCompanion({
|
||||
this.productId = const Value.absent(),
|
||||
this.localPath = const Value.absent(),
|
||||
this.createdAt = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
});
|
||||
PendingImageUploadsCompanion.insert({
|
||||
required String productId,
|
||||
required String localPath,
|
||||
required DateTime createdAt,
|
||||
this.rowid = const Value.absent(),
|
||||
}) : productId = Value(productId),
|
||||
localPath = Value(localPath),
|
||||
createdAt = Value(createdAt);
|
||||
static Insertable<PendingImageUploadRow> custom({
|
||||
Expression<String>? productId,
|
||||
Expression<String>? localPath,
|
||||
Expression<DateTime>? createdAt,
|
||||
Expression<int>? rowid,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (productId != null) 'product_id': productId,
|
||||
if (localPath != null) 'local_path': localPath,
|
||||
if (createdAt != null) 'created_at': createdAt,
|
||||
if (rowid != null) 'rowid': rowid,
|
||||
});
|
||||
}
|
||||
|
||||
PendingImageUploadsCompanion copyWith({
|
||||
Value<String>? productId,
|
||||
Value<String>? localPath,
|
||||
Value<DateTime>? createdAt,
|
||||
Value<int>? rowid,
|
||||
}) {
|
||||
return PendingImageUploadsCompanion(
|
||||
productId: productId ?? this.productId,
|
||||
localPath: localPath ?? this.localPath,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
rowid: rowid ?? this.rowid,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (productId.present) {
|
||||
map['product_id'] = Variable<String>(productId.value);
|
||||
}
|
||||
if (localPath.present) {
|
||||
map['local_path'] = Variable<String>(localPath.value);
|
||||
}
|
||||
if (createdAt.present) {
|
||||
map['created_at'] = Variable<DateTime>(createdAt.value);
|
||||
}
|
||||
if (rowid.present) {
|
||||
map['rowid'] = Variable<int>(rowid.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('PendingImageUploadsCompanion(')
|
||||
..write('productId: $productId, ')
|
||||
..write('localPath: $localPath, ')
|
||||
..write('createdAt: $createdAt, ')
|
||||
..write('rowid: $rowid')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$AppDatabase extends GeneratedDatabase {
|
||||
_$AppDatabase(QueryExecutor e) : super(e);
|
||||
$AppDatabaseManager get managers => $AppDatabaseManager(this);
|
||||
@@ -4469,6 +4795,8 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
||||
late final $TabEventsTable tabEvents = $TabEventsTable(this);
|
||||
late final $StockAdjustmentResultsTable stockAdjustmentResults =
|
||||
$StockAdjustmentResultsTable(this);
|
||||
late final $PendingImageUploadsTable pendingImageUploads =
|
||||
$PendingImageUploadsTable(this);
|
||||
@override
|
||||
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||
@@ -4485,6 +4813,7 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
||||
processedRemoteCommands,
|
||||
tabEvents,
|
||||
stockAdjustmentResults,
|
||||
pendingImageUploads,
|
||||
];
|
||||
@override
|
||||
StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules([
|
||||
@@ -4515,6 +4844,7 @@ typedef $$ProductsTableCreateCompanionBuilder =
|
||||
Value<bool> infiniteStock,
|
||||
required int priceInCents,
|
||||
Value<String?> imagePath,
|
||||
Value<String?> imageId,
|
||||
Value<bool> active,
|
||||
Value<int> rowid,
|
||||
});
|
||||
@@ -4528,6 +4858,7 @@ typedef $$ProductsTableUpdateCompanionBuilder =
|
||||
Value<bool> infiniteStock,
|
||||
Value<int> priceInCents,
|
||||
Value<String?> imagePath,
|
||||
Value<String?> imageId,
|
||||
Value<bool> active,
|
||||
Value<int> rowid,
|
||||
});
|
||||
@@ -4604,6 +4935,11 @@ class $$ProductsTableFilterComposer
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<String> get imageId => $composableBuilder(
|
||||
column: $table.imageId,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<bool> get active => $composableBuilder(
|
||||
column: $table.active,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
@@ -4684,6 +5020,11 @@ class $$ProductsTableOrderingComposer
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<String> get imageId => $composableBuilder(
|
||||
column: $table.imageId,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<bool> get active => $composableBuilder(
|
||||
column: $table.active,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
@@ -4731,6 +5072,9 @@ class $$ProductsTableAnnotationComposer
|
||||
GeneratedColumn<String> get imagePath =>
|
||||
$composableBuilder(column: $table.imagePath, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<String> get imageId =>
|
||||
$composableBuilder(column: $table.imageId, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<bool> get active =>
|
||||
$composableBuilder(column: $table.active, builder: (column) => column);
|
||||
|
||||
@@ -4796,6 +5140,7 @@ class $$ProductsTableTableManager
|
||||
Value<bool> infiniteStock = const Value.absent(),
|
||||
Value<int> priceInCents = const Value.absent(),
|
||||
Value<String?> imagePath = const Value.absent(),
|
||||
Value<String?> imageId = const Value.absent(),
|
||||
Value<bool> active = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) => ProductsCompanion(
|
||||
@@ -4807,6 +5152,7 @@ class $$ProductsTableTableManager
|
||||
infiniteStock: infiniteStock,
|
||||
priceInCents: priceInCents,
|
||||
imagePath: imagePath,
|
||||
imageId: imageId,
|
||||
active: active,
|
||||
rowid: rowid,
|
||||
),
|
||||
@@ -4820,6 +5166,7 @@ class $$ProductsTableTableManager
|
||||
Value<bool> infiniteStock = const Value.absent(),
|
||||
required int priceInCents,
|
||||
Value<String?> imagePath = const Value.absent(),
|
||||
Value<String?> imageId = const Value.absent(),
|
||||
Value<bool> active = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) => ProductsCompanion.insert(
|
||||
@@ -4831,6 +5178,7 @@ class $$ProductsTableTableManager
|
||||
infiniteStock: infiniteStock,
|
||||
priceInCents: priceInCents,
|
||||
imagePath: imagePath,
|
||||
imageId: imageId,
|
||||
active: active,
|
||||
rowid: rowid,
|
||||
),
|
||||
@@ -7549,6 +7897,184 @@ typedef $$StockAdjustmentResultsTableProcessedTableManager =
|
||||
StockAdjustmentResultRow,
|
||||
PrefetchHooks Function()
|
||||
>;
|
||||
typedef $$PendingImageUploadsTableCreateCompanionBuilder =
|
||||
PendingImageUploadsCompanion Function({
|
||||
required String productId,
|
||||
required String localPath,
|
||||
required DateTime createdAt,
|
||||
Value<int> rowid,
|
||||
});
|
||||
typedef $$PendingImageUploadsTableUpdateCompanionBuilder =
|
||||
PendingImageUploadsCompanion Function({
|
||||
Value<String> productId,
|
||||
Value<String> localPath,
|
||||
Value<DateTime> createdAt,
|
||||
Value<int> rowid,
|
||||
});
|
||||
|
||||
class $$PendingImageUploadsTableFilterComposer
|
||||
extends Composer<_$AppDatabase, $PendingImageUploadsTable> {
|
||||
$$PendingImageUploadsTableFilterComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
ColumnFilters<String> get productId => $composableBuilder(
|
||||
column: $table.productId,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<String> get localPath => $composableBuilder(
|
||||
column: $table.localPath,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<DateTime> get createdAt => $composableBuilder(
|
||||
column: $table.createdAt,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $$PendingImageUploadsTableOrderingComposer
|
||||
extends Composer<_$AppDatabase, $PendingImageUploadsTable> {
|
||||
$$PendingImageUploadsTableOrderingComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
ColumnOrderings<String> get productId => $composableBuilder(
|
||||
column: $table.productId,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<String> get localPath => $composableBuilder(
|
||||
column: $table.localPath,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
|
||||
column: $table.createdAt,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $$PendingImageUploadsTableAnnotationComposer
|
||||
extends Composer<_$AppDatabase, $PendingImageUploadsTable> {
|
||||
$$PendingImageUploadsTableAnnotationComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
GeneratedColumn<String> get productId =>
|
||||
$composableBuilder(column: $table.productId, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<String> get localPath =>
|
||||
$composableBuilder(column: $table.localPath, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<DateTime> get createdAt =>
|
||||
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||
}
|
||||
|
||||
class $$PendingImageUploadsTableTableManager
|
||||
extends
|
||||
RootTableManager<
|
||||
_$AppDatabase,
|
||||
$PendingImageUploadsTable,
|
||||
PendingImageUploadRow,
|
||||
$$PendingImageUploadsTableFilterComposer,
|
||||
$$PendingImageUploadsTableOrderingComposer,
|
||||
$$PendingImageUploadsTableAnnotationComposer,
|
||||
$$PendingImageUploadsTableCreateCompanionBuilder,
|
||||
$$PendingImageUploadsTableUpdateCompanionBuilder,
|
||||
(
|
||||
PendingImageUploadRow,
|
||||
BaseReferences<
|
||||
_$AppDatabase,
|
||||
$PendingImageUploadsTable,
|
||||
PendingImageUploadRow
|
||||
>,
|
||||
),
|
||||
PendingImageUploadRow,
|
||||
PrefetchHooks Function()
|
||||
> {
|
||||
$$PendingImageUploadsTableTableManager(
|
||||
_$AppDatabase db,
|
||||
$PendingImageUploadsTable table,
|
||||
) : super(
|
||||
TableManagerState(
|
||||
db: db,
|
||||
table: table,
|
||||
createFilteringComposer: () =>
|
||||
$$PendingImageUploadsTableFilterComposer($db: db, $table: table),
|
||||
createOrderingComposer: () =>
|
||||
$$PendingImageUploadsTableOrderingComposer(
|
||||
$db: db,
|
||||
$table: table,
|
||||
),
|
||||
createComputedFieldComposer: () =>
|
||||
$$PendingImageUploadsTableAnnotationComposer(
|
||||
$db: db,
|
||||
$table: table,
|
||||
),
|
||||
updateCompanionCallback:
|
||||
({
|
||||
Value<String> productId = const Value.absent(),
|
||||
Value<String> localPath = const Value.absent(),
|
||||
Value<DateTime> createdAt = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) => PendingImageUploadsCompanion(
|
||||
productId: productId,
|
||||
localPath: localPath,
|
||||
createdAt: createdAt,
|
||||
rowid: rowid,
|
||||
),
|
||||
createCompanionCallback:
|
||||
({
|
||||
required String productId,
|
||||
required String localPath,
|
||||
required DateTime createdAt,
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) => PendingImageUploadsCompanion.insert(
|
||||
productId: productId,
|
||||
localPath: localPath,
|
||||
createdAt: createdAt,
|
||||
rowid: rowid,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
|
||||
.toList(),
|
||||
prefetchHooksCallback: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
typedef $$PendingImageUploadsTableProcessedTableManager =
|
||||
ProcessedTableManager<
|
||||
_$AppDatabase,
|
||||
$PendingImageUploadsTable,
|
||||
PendingImageUploadRow,
|
||||
$$PendingImageUploadsTableFilterComposer,
|
||||
$$PendingImageUploadsTableOrderingComposer,
|
||||
$$PendingImageUploadsTableAnnotationComposer,
|
||||
$$PendingImageUploadsTableCreateCompanionBuilder,
|
||||
$$PendingImageUploadsTableUpdateCompanionBuilder,
|
||||
(
|
||||
PendingImageUploadRow,
|
||||
BaseReferences<
|
||||
_$AppDatabase,
|
||||
$PendingImageUploadsTable,
|
||||
PendingImageUploadRow
|
||||
>,
|
||||
),
|
||||
PendingImageUploadRow,
|
||||
PrefetchHooks Function()
|
||||
>;
|
||||
|
||||
class $AppDatabaseManager {
|
||||
final _$AppDatabase _db;
|
||||
@@ -7581,4 +8107,6 @@ class $AppDatabaseManager {
|
||||
_db,
|
||||
_db.stockAdjustmentResults,
|
||||
);
|
||||
$$PendingImageUploadsTableTableManager get pendingImageUploads =>
|
||||
$$PendingImageUploadsTableTableManager(_db, _db.pendingImageUploads);
|
||||
}
|
||||
|
||||
@@ -1,73 +1,184 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
import '../models/remote_command.dart';
|
||||
import '../models/tab_event.dart';
|
||||
import 'app_database.dart';
|
||||
|
||||
/// The product catalog as the website manages it.
|
||||
/// A product as the catalog and this tablet's unsent changes make it.
|
||||
class _Product {
|
||||
String name;
|
||||
String category;
|
||||
int priceInCents;
|
||||
int lowStockThreshold;
|
||||
bool active;
|
||||
String? imageId;
|
||||
String? imagePath;
|
||||
|
||||
_Product(ProductRow row)
|
||||
: name = row.name,
|
||||
category = row.category,
|
||||
priceInCents = row.priceInCents,
|
||||
lowStockThreshold = row.lowStockThreshold,
|
||||
active = row.active,
|
||||
imageId = row.imageId,
|
||||
imagePath = row.imagePath;
|
||||
|
||||
_Product.fromServer(ServerProduct product)
|
||||
: name = product.name,
|
||||
category = product.category ?? '',
|
||||
priceInCents = product.priceInCents,
|
||||
lowStockThreshold = product.lowStockThreshold ?? 0,
|
||||
active = !product.archived,
|
||||
imageId = product.imageId,
|
||||
imagePath = null;
|
||||
|
||||
bool sameAs(ProductRow row) =>
|
||||
name == row.name &&
|
||||
category == row.category &&
|
||||
priceInCents == row.priceInCents &&
|
||||
lowStockThreshold == row.lowStockThreshold &&
|
||||
active == row.active &&
|
||||
imageId == row.imageId &&
|
||||
imagePath == row.imagePath;
|
||||
|
||||
ProductsCompanion toCompanion() => ProductsCompanion(
|
||||
name: Value(name),
|
||||
category: Value(category),
|
||||
priceInCents: Value(priceInCents),
|
||||
lowStockThreshold: Value(lowStockThreshold),
|
||||
active: Value(active),
|
||||
imageId: Value(imageId),
|
||||
imagePath: Value(imagePath),
|
||||
);
|
||||
|
||||
/// Applies the fields of a `product_created` / `product_updated` event.
|
||||
void apply(Map<String, dynamic> fields) {
|
||||
if (fields['name'] case final String value) name = value;
|
||||
if (fields.containsKey('category')) {
|
||||
category = fields['category'] as String? ?? '';
|
||||
}
|
||||
if (fields['priceInCents'] case final int value) priceInCents = value;
|
||||
if (fields.containsKey('lowStockThreshold')) {
|
||||
lowStockThreshold = fields['lowStockThreshold'] as int? ?? 0;
|
||||
}
|
||||
if (fields.containsKey('imageId')) imageId = fields['imageId'] as String?;
|
||||
}
|
||||
}
|
||||
|
||||
/// The product catalog: the website's, with this tablet's changes on top.
|
||||
extension CatalogQueries on AppDatabase {
|
||||
/// Makes the local products match [catalog]: new products are added,
|
||||
/// changed ones updated, archived ones and products the server doesn't
|
||||
/// know (such as the default products of a new tablet) are hidden. Hidden,
|
||||
/// not deleted: open tabs and the history refer to them.
|
||||
/// Makes the local products match [catalog], then replays this tablet's
|
||||
/// product changes the server hasn't applied yet on top, so an offline
|
||||
/// edit stays visible until the server has it (the same idea as the tabs).
|
||||
///
|
||||
/// The stock is not touched here: the stock rebase that follows sets it, a
|
||||
/// new product starts as untracked until then. Local images are kept.
|
||||
/// New products are added; archived ones, and products the server doesn't
|
||||
/// know and this tablet didn't create (such as the default products of a
|
||||
/// new tablet), are hidden: open tabs and the history refer to them.
|
||||
///
|
||||
/// Pictures: a changed one keeps showing the old file until
|
||||
/// [ProductImageSync] downloaded the new one; a picture waiting to be
|
||||
/// uploaded from this tablet is left alone. The stock is not touched here:
|
||||
/// the stock rebase that follows sets it.
|
||||
Future<void> applyServerCatalog(List<ServerProduct> catalog) {
|
||||
return transaction(() async {
|
||||
final local = {
|
||||
final rows = {
|
||||
for (final row in await select(products).get()) row.id: row,
|
||||
};
|
||||
final uploading = {
|
||||
for (final row in await select(pendingImageUploads).get())
|
||||
row.productId,
|
||||
};
|
||||
final desired = {for (final row in rows.values) row.id: _Product(row)};
|
||||
|
||||
final listed = <String>{};
|
||||
for (final product in catalog) {
|
||||
final category = product.category ?? '';
|
||||
final lowStockThreshold = product.lowStockThreshold ?? 0;
|
||||
final existing = local.remove(product.id);
|
||||
|
||||
if (existing == null) {
|
||||
await into(products).insert(
|
||||
ProductsCompanion.insert(
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
category: category,
|
||||
stockQuantity: 0,
|
||||
lowStockThreshold: lowStockThreshold,
|
||||
infiniteStock: const Value(true),
|
||||
priceInCents: product.priceInCents,
|
||||
active: Value(!product.archived),
|
||||
),
|
||||
);
|
||||
listed.add(product.id);
|
||||
final local = desired[product.id];
|
||||
final next = _Product.fromServer(product);
|
||||
if (local == null) {
|
||||
desired[product.id] = next;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (existing.name == product.name &&
|
||||
existing.category == category &&
|
||||
existing.priceInCents == product.priceInCents &&
|
||||
existing.lowStockThreshold == lowStockThreshold &&
|
||||
existing.active == !product.archived) {
|
||||
continue;
|
||||
if (uploading.contains(product.id)) {
|
||||
next
|
||||
..imageId = local.imageId
|
||||
..imagePath = local.imagePath;
|
||||
} else if (next.imageId != null && next.imageId == local.imageId) {
|
||||
next.imagePath = local.imagePath;
|
||||
} else if (next.imageId != null) {
|
||||
// Until the new picture is downloaded, the old one is better than none
|
||||
next.imagePath = local.imagePath;
|
||||
}
|
||||
await (update(
|
||||
products,
|
||||
)..where((row) => row.id.equals(product.id))).write(
|
||||
ProductsCompanion(
|
||||
name: Value(product.name),
|
||||
category: Value(category),
|
||||
priceInCents: Value(product.priceInCents),
|
||||
lowStockThreshold: Value(lowStockThreshold),
|
||||
active: Value(!product.archived),
|
||||
),
|
||||
);
|
||||
desired[product.id] = next;
|
||||
}
|
||||
for (final entry in desired.entries) {
|
||||
if (!listed.contains(entry.key)) entry.value.active = false;
|
||||
}
|
||||
|
||||
final unknown = [
|
||||
for (final row in local.values)
|
||||
if (row.active) row.id,
|
||||
];
|
||||
if (unknown.isNotEmpty) {
|
||||
await (update(products)..where((row) => row.id.isIn(unknown))).write(
|
||||
const ProductsCompanion(active: Value(false)),
|
||||
);
|
||||
for (final event in await _pendingProductEvents()) {
|
||||
final productId = event['productId'] as String?;
|
||||
final local = productId == null ? null : desired[productId];
|
||||
if (local == null) continue;
|
||||
switch (TabEventType.fromValue(event['type'] as String? ?? '')) {
|
||||
case TabEventType.productCreated || TabEventType.productUpdated:
|
||||
local.apply(event['product'] as Map<String, dynamic>? ?? {});
|
||||
if (event['type'] == TabEventType.productCreated.value) {
|
||||
local.active = true;
|
||||
}
|
||||
case TabEventType.productArchived:
|
||||
local.active = false;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (final MapEntry(key: id, value: product) in desired.entries) {
|
||||
final row = rows[id];
|
||||
if (row == null) {
|
||||
await into(products).insert(
|
||||
product.toCompanion().copyWith(
|
||||
id: Value(id),
|
||||
// Untracked until the stock rebase sets its stock
|
||||
stockQuantity: const Value(0),
|
||||
infiniteStock: const Value(true),
|
||||
),
|
||||
);
|
||||
} else if (!product.sameAs(row)) {
|
||||
await (update(
|
||||
products,
|
||||
)..where((r) => r.id.equals(id))).write(product.toCompanion());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Queues a picture taken on this tablet for upload; a newer picture of the
|
||||
/// same product replaces one still waiting.
|
||||
Future<void> queueImageUpload(String productId, String localPath) async {
|
||||
await into(pendingImageUploads).insertOnConflictUpdate(
|
||||
PendingImageUploadsCompanion.insert(
|
||||
productId: productId,
|
||||
localPath: localPath,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> _pendingProductEvents() async {
|
||||
final types = TabEventType.products.map((type) => type.value).toSet();
|
||||
final rows =
|
||||
await (select(tabEvents)..orderBy([
|
||||
(row) => OrderingTerm.asc(row.createdAt),
|
||||
(row) => OrderingTerm.asc(const CustomExpression<int>('rowid')),
|
||||
]))
|
||||
.get();
|
||||
return [
|
||||
for (final row in rows)
|
||||
if (jsonDecode(row.payload) case final Map<String, dynamic> event
|
||||
when types.contains(event['type']))
|
||||
event,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,10 +50,10 @@ extension StockQueries on AppDatabase {
|
||||
);
|
||||
}
|
||||
|
||||
/// Queues a `stock_set` event, so a stock count or tracking change made on
|
||||
/// this tablet reaches the server. Call it inside the transaction that
|
||||
/// writes the stock.
|
||||
Future<void> queueStockSet(TabEvent event) async {
|
||||
/// Queues an event that is not about a tab (a stock count, a product
|
||||
/// change) in the outbox. Call it inside the transaction that makes the
|
||||
/// change, so the change and its event are stored together.
|
||||
Future<void> queueEvent(TabEvent event) async {
|
||||
await into(tabEvents).insert(
|
||||
TabEventsCompanion.insert(
|
||||
id: event.id,
|
||||
|
||||
Reference in New Issue
Block a user