feat(labels): choose copies per label, prefilled from container count
Each label in the print sheet gets an editable copy count, suggested from the lot's latest condition-check container count (3 jars → 3 labels), and falling back to 1. Type or step the count; the total updates live and Save is disabled at zero. On save each seed expands into that many identical labels. - SeedLabelEntry.suggestedCopies; labelRows reads the latest container count - label print sheet: per-row −/number/+ stepper, live total, expand on save - tests: repository suggestedCopies + print-sheet copy chooser
This commit is contained in:
parent
63f48db5c2
commit
d36fd05741
4 changed files with 298 additions and 4 deletions
|
|
@ -134,6 +134,7 @@ class SeedLabelEntry extends Equatable {
|
|||
this.quantity,
|
||||
this.originName,
|
||||
this.originPlace,
|
||||
this.suggestedCopies = 1,
|
||||
});
|
||||
|
||||
final String varietyLabel;
|
||||
|
|
@ -147,6 +148,11 @@ class SeedLabelEntry extends Equatable {
|
|||
final String? originName;
|
||||
final String? originPlace;
|
||||
|
||||
/// How many copies of this label to suggest — the latest condition check's
|
||||
/// container count (e.g. 3 jars → 3 labels), or 1 when unknown. Always ≥ 1;
|
||||
/// the print sheet prefills it and lets the keeper edit.
|
||||
final int suggestedCopies;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
varietyLabel,
|
||||
|
|
@ -157,6 +163,7 @@ class SeedLabelEntry extends Equatable {
|
|||
quantity,
|
||||
originName,
|
||||
originPlace,
|
||||
suggestedCopies,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -1325,6 +1332,28 @@ class VarietyRepository {
|
|||
(lotsByVariety[l.varietyId] ??= <Lot>[]).add(l);
|
||||
}
|
||||
|
||||
// Suggest one label per stored container: the latest condition check's
|
||||
// container count (3 jars → 3 labels). `.first` is the most recent because
|
||||
// we order by check date descending.
|
||||
final containersByLot = <String, int>{};
|
||||
if (lots.isNotEmpty) {
|
||||
final checks =
|
||||
await (_db.select(_db.conditionChecks)
|
||||
..where(
|
||||
(c) =>
|
||||
c.lotId.isIn(lots.map((l) => l.id).toList()) &
|
||||
c.isDeleted.equals(false),
|
||||
)
|
||||
..orderBy([(c) => OrderingTerm.desc(c.checkedOn)]))
|
||||
.get();
|
||||
for (final c in checks) {
|
||||
final count = c.containerCount;
|
||||
if (count != null && count > 0) {
|
||||
containersByLot.putIfAbsent(c.lotId, () => count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SeedLabelEntry entryFor(Variety v, Lot? l) {
|
||||
final hasQuantity =
|
||||
l != null &&
|
||||
|
|
@ -1346,6 +1375,7 @@ class VarietyRepository {
|
|||
: null,
|
||||
originName: l?.originName,
|
||||
originPlace: l?.originPlace,
|
||||
suggestedCopies: l == null ? 1 : (containersByLot[l.id] ?? 1),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../data/export_import/seed_label_codec.dart';
|
||||
import '../data/variety_repository.dart';
|
||||
|
|
@ -8,7 +9,8 @@ import '../services/label_sheet_service.dart';
|
|||
import 'quantity_kind_l10n.dart';
|
||||
|
||||
/// Opens the print-labels sheet for [entries] (already gathered from the
|
||||
/// current selection): pick a label size, then save a PDF sheet of labels —
|
||||
/// current selection): choose how many copies of each label (prefilled from the
|
||||
/// stored container count) and a label size, then save a PDF sheet of labels —
|
||||
/// each with a QR that carries the seed's data.
|
||||
Future<void> showLabelPrintSheet(
|
||||
BuildContext context,
|
||||
|
|
@ -21,6 +23,9 @@ Future<void> showLabelPrintSheet(
|
|||
);
|
||||
}
|
||||
|
||||
/// Upper bound on copies per label — a printed sheet, not a print run.
|
||||
const _maxCopies = 99;
|
||||
|
||||
class _LabelPrintSheet extends StatefulWidget {
|
||||
const _LabelPrintSheet({required this.entries});
|
||||
|
||||
|
|
@ -34,6 +39,24 @@ class _LabelPrintSheetState extends State<_LabelPrintSheet> {
|
|||
LabelSheetFormat _format = LabelSheetFormat.stickers;
|
||||
bool _saving = false;
|
||||
|
||||
/// One editable copy-count per entry, prefilled from its suggested count.
|
||||
late final List<int> _copies = [
|
||||
for (final e in widget.entries) e.suggestedCopies.clamp(0, _maxCopies),
|
||||
];
|
||||
late final List<TextEditingController> _controllers = [
|
||||
for (final n in _copies) TextEditingController(text: '$n'),
|
||||
];
|
||||
|
||||
int get _total => _copies.fold(0, (sum, n) => sum + n);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final c in _controllers) {
|
||||
c.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.t;
|
||||
|
|
@ -50,10 +73,32 @@ class _LabelPrintSheetState extends State<_LabelPrintSheet> {
|
|||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
t.printLabels.count(n: widget.entries.length),
|
||||
t.printLabels.count(n: _total),
|
||||
key: const Key('printLabels.total'),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// The per-seed copy list can be long; keep the format chooser and
|
||||
// the save button always visible by scrolling only this section.
|
||||
Flexible(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.4,
|
||||
),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: widget.entries.length,
|
||||
itemBuilder: (context, i) => _CopyRow(
|
||||
entry: widget.entries[i],
|
||||
controller: _controllers[i],
|
||||
onMinus: _copies[i] > 0 ? () => _bump(i, -1) : null,
|
||||
onPlus: _copies[i] < _maxCopies ? () => _bump(i, 1) : null,
|
||||
onChanged: (value) => _setCopies(i, value),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(height: 20),
|
||||
RadioGroup<LabelSheetFormat>(
|
||||
groupValue: _format,
|
||||
onChanged: _pick,
|
||||
|
|
@ -82,7 +127,7 @@ class _LabelPrintSheetState extends State<_LabelPrintSheet> {
|
|||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: FilledButton.icon(
|
||||
key: const Key('printLabels.save'),
|
||||
onPressed: _saving ? null : _save,
|
||||
onPressed: (_saving || _total == 0) ? null : _save,
|
||||
icon: const Icon(Icons.save_alt_outlined),
|
||||
label: Text(t.printLabels.save),
|
||||
),
|
||||
|
|
@ -97,6 +142,19 @@ class _LabelPrintSheetState extends State<_LabelPrintSheet> {
|
|||
if (value != null) setState(() => _format = value);
|
||||
}
|
||||
|
||||
/// Steps the count for entry [i] by [delta], syncing the text field.
|
||||
void _bump(int i, int delta) {
|
||||
final next = (_copies[i] + delta).clamp(0, _maxCopies);
|
||||
_controllers[i].text = '$next';
|
||||
setState(() => _copies[i] = next);
|
||||
}
|
||||
|
||||
/// Applies a typed value (empty → 0), clamped to the allowed range.
|
||||
void _setCopies(int i, String value) {
|
||||
final parsed = int.tryParse(value) ?? 0;
|
||||
setState(() => _copies[i] = parsed.clamp(0, _maxCopies));
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
final t = context.t;
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
|
|
@ -104,7 +162,11 @@ class _LabelPrintSheetState extends State<_LabelPrintSheet> {
|
|||
final rtl = Directionality.of(context) == TextDirection.rtl;
|
||||
setState(() => _saving = true);
|
||||
|
||||
final labels = [for (final e in widget.entries) _toLabel(t, e)];
|
||||
// Expand each entry into its chosen number of identical labels.
|
||||
final labels = <LabelSheetLabel>[
|
||||
for (var i = 0; i < widget.entries.length; i++)
|
||||
for (var c = 0; c < _copies[i]; c++) _toLabel(t, widget.entries[i]),
|
||||
];
|
||||
final saved = await getIt<LabelSheetService>().saveLabels(
|
||||
labels: labels,
|
||||
format: _format,
|
||||
|
|
@ -158,3 +220,87 @@ class _LabelPrintSheetState extends State<_LabelPrintSheet> {
|
|||
|
||||
String _today() => DateTime.now().toIso8601String().substring(0, 10);
|
||||
}
|
||||
|
||||
/// One row of the copy chooser: the seed's name plus a −/number/+ stepper. The
|
||||
/// number is directly editable so "12 jars" is a quick type, not twelve taps.
|
||||
class _CopyRow extends StatelessWidget {
|
||||
const _CopyRow({
|
||||
required this.entry,
|
||||
required this.controller,
|
||||
required this.onMinus,
|
||||
required this.onPlus,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
final SeedLabelEntry entry;
|
||||
final TextEditingController controller;
|
||||
final VoidCallback? onMinus;
|
||||
final VoidCallback? onPlus;
|
||||
final ValueChanged<String> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final subtitle = <String>[
|
||||
if (entry.commonName != null && entry.commonName!.trim().isNotEmpty)
|
||||
entry.commonName!,
|
||||
if (entry.harvestYear != null) '${entry.harvestYear}',
|
||||
].join(' · ');
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
entry.varietyLabel,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodyLarge,
|
||||
),
|
||||
if (subtitle.isNotEmpty)
|
||||
Text(
|
||||
subtitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
key: Key('printLabels.copies.minus.${entry.varietyLabel}'),
|
||||
onPressed: onMinus,
|
||||
icon: const Icon(Icons.remove_circle_outline),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
SizedBox(
|
||||
width: 44,
|
||||
child: TextField(
|
||||
key: Key('printLabels.copies.field.${entry.varietyLabel}'),
|
||||
controller: controller,
|
||||
onChanged: onChanged,
|
||||
textAlign: TextAlign.center,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
decoration: const InputDecoration(
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
key: Key('printLabels.copies.plus.${entry.varietyLabel}'),
|
||||
onPressed: onPlus,
|
||||
icon: const Icon(Icons.add_circle_outline),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue