feat: type stats and event log models
This commit is contained in:
141
lib/models/stats_summary.dart
Normal file
141
lib/models/stats_summary.dart
Normal file
@@ -0,0 +1,141 @@
|
||||
class StatsSummary {
|
||||
final List<GroupStats> groups;
|
||||
|
||||
const StatsSummary({required this.groups});
|
||||
|
||||
static const empty = StatsSummary(groups: <GroupStats>[]);
|
||||
|
||||
static StatsSummary fromApi(Object? data) {
|
||||
return StatsSummary(groups: GroupStats.listFromApi(data));
|
||||
}
|
||||
}
|
||||
|
||||
class GroupStats {
|
||||
final String targetId;
|
||||
final String name;
|
||||
final int totalCommands;
|
||||
final int togglesOn;
|
||||
final int togglesOff;
|
||||
final double? estimatedHours;
|
||||
|
||||
const GroupStats({
|
||||
required this.targetId,
|
||||
required this.name,
|
||||
required this.totalCommands,
|
||||
required this.togglesOn,
|
||||
required this.togglesOff,
|
||||
this.estimatedHours,
|
||||
});
|
||||
|
||||
String get formattedEstimatedHours {
|
||||
final value = estimatedHours;
|
||||
if (value == null) return '';
|
||||
if (value < 1) return '${(value * 60).round()} мин';
|
||||
return '${value.toStringAsFixed(1)} ч';
|
||||
}
|
||||
|
||||
static GroupStats fromApi(Object? data, {String? fallbackId}) {
|
||||
if (data is! Map) {
|
||||
final id = data?.toString() ?? fallbackId;
|
||||
if (id == null || id.isEmpty) {
|
||||
throw const FormatException('group stats должен быть объектом');
|
||||
}
|
||||
return GroupStats(
|
||||
targetId: id,
|
||||
name: id,
|
||||
totalCommands: 0,
|
||||
togglesOn: 0,
|
||||
togglesOff: 0,
|
||||
);
|
||||
}
|
||||
|
||||
final map = Map<String, dynamic>.from(data);
|
||||
final targetId =
|
||||
_stringValue(map, const ['target_id', 'group_id', 'id', 'target']) ??
|
||||
fallbackId;
|
||||
if (targetId == null || targetId.isEmpty) {
|
||||
throw const FormatException('group stats не содержит id');
|
||||
}
|
||||
|
||||
return GroupStats(
|
||||
targetId: targetId,
|
||||
name: _stringValue(map, const ['name', 'label']) ?? targetId,
|
||||
totalCommands: _intValue(map['total_commands']),
|
||||
togglesOn: _intValue(map['toggles_on']),
|
||||
togglesOff: _intValue(map['toggles_off']),
|
||||
estimatedHours: _doubleValue(map['estimated_hours']),
|
||||
);
|
||||
}
|
||||
|
||||
static List<GroupStats> listFromApi(Object? data) {
|
||||
final values = _collectionValues(data, const ['groups', 'data', 'items']);
|
||||
return values.map((value) {
|
||||
if (value.entryKey == null) return GroupStats.fromApi(value.value);
|
||||
return GroupStats.fromApi(value.value, fallbackId: value.entryKey);
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
||||
String? _stringValue(Map<String, dynamic> map, List<String> keys) {
|
||||
for (final key in keys) {
|
||||
final value = map[key];
|
||||
if (value != null && value.toString().isNotEmpty) {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
int _intValue(Object? value) {
|
||||
if (value is int) return value;
|
||||
if (value is num) return value.round();
|
||||
if (value is String) return int.tryParse(value.trim()) ?? 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
double? _doubleValue(Object? value) {
|
||||
if (value is num) return value.toDouble();
|
||||
if (value is String) return double.tryParse(value.trim());
|
||||
return null;
|
||||
}
|
||||
|
||||
List<_CollectionValue> _collectionValues(Object? data, List<String> wrappers) {
|
||||
if (data is List) {
|
||||
return data.map((value) => _CollectionValue(value)).toList();
|
||||
}
|
||||
|
||||
if (data is Map) {
|
||||
final map = Map<String, dynamic>.from(data);
|
||||
for (final wrapper in wrappers) {
|
||||
final value = map[wrapper];
|
||||
if (value is List) {
|
||||
return value.map((item) => _CollectionValue(item)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
if (_looksLikeStatsItem(map)) {
|
||||
return <_CollectionValue>[_CollectionValue(map)];
|
||||
}
|
||||
|
||||
return map.entries
|
||||
.where((entry) => entry.value is Map)
|
||||
.map((entry) => _CollectionValue(entry.value, entryKey: entry.key))
|
||||
.toList();
|
||||
}
|
||||
|
||||
throw const FormatException('stats summary должен быть объектом или списком');
|
||||
}
|
||||
|
||||
bool _looksLikeStatsItem(Map<String, dynamic> map) {
|
||||
return map.containsKey('total_commands') ||
|
||||
map.containsKey('toggles_on') ||
|
||||
map.containsKey('toggles_off') ||
|
||||
map.containsKey('estimated_hours');
|
||||
}
|
||||
|
||||
class _CollectionValue {
|
||||
final Object? value;
|
||||
final String? entryKey;
|
||||
|
||||
const _CollectionValue(this.value, {this.entryKey});
|
||||
}
|
||||
Reference in New Issue
Block a user