feat(inventory): editable vernacular (common) names
The "also known as" names were read-only; now they can be added and removed: - Repository: addVernacularName + removeVernacularName (soft delete); VarietyDetail.vernacularNames carries ids (new VernacularName model). - Cubit: addVernacularName / removeVernacularName. - Detail: deletable name chips + an "Add name" chip opening a small dialog; the section is always visible so names can be added when empty. Tests: repo add/remove + widget add/remove. 44 green.
This commit is contained in:
parent
f8d4d9a778
commit
9e5c82184b
10 changed files with 256 additions and 83 deletions
|
|
@ -76,17 +76,28 @@ class _DetailView extends StatelessWidget {
|
|||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_DetailHeader(detail: detail),
|
||||
if (detail.vernacularNames.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
_SectionTitle(t.detail.names),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
for (final name in detail.vernacularNames)
|
||||
Chip(label: Text(name)),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
_SectionTitle(t.detail.names),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
for (final name in detail.vernacularNames)
|
||||
InputChip(
|
||||
key: Key('name.${name.id}'),
|
||||
label: Text(name.name),
|
||||
deleteIcon: const Icon(Icons.close, size: 18),
|
||||
onDeleted: () => cubit.removeVernacularName(name.id),
|
||||
),
|
||||
ActionChip(
|
||||
key: const Key('name.add'),
|
||||
avatar: const Icon(Icons.add, size: 18),
|
||||
label: Text(t.detail.addName),
|
||||
onPressed: () => _showAddNameDialog(context, cubit),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (detail.notes != null && detail.notes!.trim().isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
_SectionTitle(t.detail.notes),
|
||||
|
|
@ -400,77 +411,79 @@ class _EditVarietySheetState extends State<_EditVarietySheet> {
|
|||
top: 16,
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom + 16,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
t.editVariety.title,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
key: const Key('editVariety.name'),
|
||||
controller: _name,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.editVariety.name,
|
||||
border: const OutlineInputBorder(),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
t.editVariety.title,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
key: const Key('editVariety.species'),
|
||||
controller: _speciesField,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.editVariety.species,
|
||||
hintText: t.editVariety.speciesHint,
|
||||
prefixIcon: const Icon(Icons.eco_outlined),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
onChanged: _onSpeciesQuery,
|
||||
),
|
||||
for (final match in _suggestions)
|
||||
ListTile(
|
||||
dense: true,
|
||||
key: Key('species.option.${match.id}'),
|
||||
title: Text(match.displayLabel),
|
||||
subtitle: match.family == null ? null : Text(match.family!),
|
||||
onTap: () => _pick(match),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _category,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.editVariety.category,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _notes,
|
||||
minLines: 2,
|
||||
maxLines: 5,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.editVariety.notes,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(t.common.cancel),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
key: const Key('editVariety.name'),
|
||||
controller: _name,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.editVariety.name,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
const Spacer(),
|
||||
FilledButton(
|
||||
key: const Key('editVariety.save'),
|
||||
onPressed: _save,
|
||||
child: Text(t.common.save),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
key: const Key('editVariety.species'),
|
||||
controller: _speciesField,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.editVariety.species,
|
||||
hintText: t.editVariety.speciesHint,
|
||||
prefixIcon: const Icon(Icons.eco_outlined),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
onChanged: _onSpeciesQuery,
|
||||
),
|
||||
for (final match in _suggestions)
|
||||
ListTile(
|
||||
dense: true,
|
||||
key: Key('species.option.${match.id}'),
|
||||
title: Text(match.displayLabel),
|
||||
subtitle: match.family == null ? null : Text(match.family!),
|
||||
onTap: () => _pick(match),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _category,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.editVariety.category,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _notes,
|
||||
minLines: 2,
|
||||
maxLines: 5,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.editVariety.notes,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(t.common.cancel),
|
||||
),
|
||||
const Spacer(),
|
||||
FilledButton(
|
||||
key: const Key('editVariety.save'),
|
||||
onPressed: _save,
|
||||
child: Text(t.common.save),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -598,6 +611,44 @@ Future<void> _showLotSheet(
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> _showAddNameDialog(
|
||||
BuildContext context,
|
||||
VarietyDetailCubit cubit,
|
||||
) {
|
||||
final t = context.t;
|
||||
final controller = TextEditingController();
|
||||
void submit(BuildContext dialogContext) {
|
||||
final name = controller.text.trim();
|
||||
if (name.isNotEmpty) cubit.addVernacularName(name);
|
||||
Navigator.of(dialogContext).pop();
|
||||
}
|
||||
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(t.detail.addName),
|
||||
content: TextField(
|
||||
key: const Key('name.field'),
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(labelText: t.editVariety.name),
|
||||
onSubmitted: (_) => submit(dialogContext),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: Text(t.common.cancel),
|
||||
),
|
||||
FilledButton(
|
||||
key: const Key('name.save'),
|
||||
onPressed: () => submit(dialogContext),
|
||||
child: Text(t.common.save),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String? _nullIfBlank(String value) {
|
||||
final trimmed = value.trim();
|
||||
return trimmed.isEmpty ? null : trimmed;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue