feat(calendar): "what's due this month" — dedicated screen + inventory filter
Reuses the existing per-variety crop-calendar masks (sow/transplant/ flowering/fruiting/seed-harvest) — no schema change, no migration — to answer "qué toca este mes" across the whole inventory. Since the months are the grower's own record, there's no hemisphere assumption. - Repo: CalendarEntry + watchCalendar() (varieties with any recorded phase); VarietyListItem now carries sowMonths. - Dedicated CalendarScreen (/calendar): a month strip (defaults to the current month, intl month names) and varieties grouped by phase — actions (sow/transplant/harvest seed) above informational (flowering/ fruiting), each tinted, tap → variety detail; empty-month state. - Inventory filter: a 'this month' chip (shown only when some variety has a calendar) keeps what sows in the current month; folded into the attributes group + clear-filters. - Entry points: a live Home card + a drawer item. - i18n calendar block + menu.calendar (en/es/pt/ast). - Tests: calendar_test (watchCalendar, list carries sow-months, pure filter), calendar_screen_test (phase grouping, month switch, empty), plus the screen added to the small-screen overflow guard.
This commit is contained in:
parent
28e8318026
commit
171daabce3
19 changed files with 681 additions and 5 deletions
|
|
@ -25,6 +25,7 @@ class VarietyListItem extends Equatable {
|
|||
this.needsReproduction = false,
|
||||
this.isShared = false,
|
||||
this.viability = SeedViability.unknown,
|
||||
this.sowMonths,
|
||||
});
|
||||
|
||||
final String id;
|
||||
|
|
@ -62,6 +63,10 @@ class VarietyListItem extends Equatable {
|
|||
/// or there is no reference figure.
|
||||
final SeedViability viability;
|
||||
|
||||
/// The grower's recorded sow-months bitmask (see `domain/crop_calendar.dart`),
|
||||
/// or null when unrecorded — drives the "to sow this month" inventory filter.
|
||||
final int? sowMonths;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
|
|
@ -75,6 +80,47 @@ class VarietyListItem extends Equatable {
|
|||
needsReproduction,
|
||||
isShared,
|
||||
viability,
|
||||
sowMonths,
|
||||
];
|
||||
}
|
||||
|
||||
/// A variety's whole recorded crop calendar, for the aggregate "what's due this
|
||||
/// month" view. Each field is a 12-bit month mask (see `crop_calendar.dart`);
|
||||
/// only varieties with at least one recorded phase are emitted.
|
||||
class CalendarEntry extends Equatable {
|
||||
const CalendarEntry({
|
||||
required this.id,
|
||||
required this.label,
|
||||
this.category,
|
||||
this.photo,
|
||||
this.sowMonths,
|
||||
this.transplantMonths,
|
||||
this.floweringMonths,
|
||||
this.fruitingMonths,
|
||||
this.seedHarvestMonths,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String label;
|
||||
final String? category;
|
||||
final Uint8List? photo;
|
||||
final int? sowMonths;
|
||||
final int? transplantMonths;
|
||||
final int? floweringMonths;
|
||||
final int? fruitingMonths;
|
||||
final int? seedHarvestMonths;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
label,
|
||||
category,
|
||||
photo,
|
||||
sowMonths,
|
||||
transplantMonths,
|
||||
floweringMonths,
|
||||
fruitingMonths,
|
||||
seedHarvestMonths,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -606,11 +652,57 @@ class VarietyRepository {
|
|||
: speciesViability[v.speciesId],
|
||||
currentYear: currentYear,
|
||||
),
|
||||
sowMonths: v.sowMonths,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Emits every non-draft variety that has any recorded crop-calendar phase,
|
||||
/// for the aggregate "what's due this month" screen. Re-emits on variety or
|
||||
/// photo changes.
|
||||
Stream<List<CalendarEntry>> watchCalendar() {
|
||||
final triggers = StreamGroup.merge<void>([
|
||||
(_db.select(
|
||||
_db.varieties,
|
||||
)..where((v) => v.isDeleted.equals(false))).watch().map((_) {}),
|
||||
_db.select(_db.attachments).watch().map((_) {}),
|
||||
]);
|
||||
return triggers.asyncMap((_) => _loadCalendar());
|
||||
}
|
||||
|
||||
Future<List<CalendarEntry>> _loadCalendar() async {
|
||||
final rows =
|
||||
await (_db.select(_db.varieties)
|
||||
..where(
|
||||
(v) =>
|
||||
v.isDeleted.equals(false) &
|
||||
v.isDraft.equals(false) &
|
||||
(v.sowMonths.isNotNull() |
|
||||
v.transplantMonths.isNotNull() |
|
||||
v.floweringMonths.isNotNull() |
|
||||
v.fruitingMonths.isNotNull() |
|
||||
v.seedHarvestMonths.isNotNull()),
|
||||
)
|
||||
..orderBy([(v) => OrderingTerm(expression: v.label)]))
|
||||
.get();
|
||||
final photos = await _firstPhotosFor(rows.map((v) => v.id).toList());
|
||||
return [
|
||||
for (final v in rows)
|
||||
CalendarEntry(
|
||||
id: v.id,
|
||||
label: v.label,
|
||||
category: v.category,
|
||||
photo: photos[v.id],
|
||||
sowMonths: v.sowMonths,
|
||||
transplantMonths: v.transplantMonths,
|
||||
floweringMonths: v.floweringMonths,
|
||||
fruitingMonths: v.fruitingMonths,
|
||||
seedHarvestMonths: v.seedHarvestMonths,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Maps each of [speciesIds] to its bundled viability figure (years); species
|
||||
/// without a figure are simply absent from the map.
|
||||
Future<Map<String, int>> _speciesViabilityFor(Set<String> speciesIds) async {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue