feat: dynamic units table with localized names via GET /units
- Add units + unit_translations tables with FK constraints on products and ingredient_mappings - Normalize products.unit from Russian strings (г, кг) to English codes (g, kg) - Load units at startup (in-memory registry) and serve via GET /units (language-aware) - Replace hardcoded _units lists and _mapUnit() functions in Flutter with unitsProvider FutureProvider - Re-fetches automatically when language changes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../core/locale/unit_provider.dart';
|
||||
import '../../shared/models/ingredient_mapping.dart';
|
||||
import 'product_provider.dart';
|
||||
|
||||
@@ -18,7 +19,7 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
final _qtyController = TextEditingController(text: '1');
|
||||
final _daysController = TextEditingController(text: '7');
|
||||
|
||||
String _unit = 'шт';
|
||||
String _unit = 'pcs';
|
||||
String? _category;
|
||||
String? _mappingId;
|
||||
bool _saving = false;
|
||||
@@ -28,8 +29,6 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
bool _searching = false;
|
||||
Timer? _debounce;
|
||||
|
||||
static const _units = ['г', 'кг', 'мл', 'л', 'шт'];
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
@@ -69,8 +68,7 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
_mappingId = mapping.id;
|
||||
_category = mapping.category;
|
||||
if (mapping.defaultUnit != null) {
|
||||
// Map backend unit codes to display units
|
||||
_unit = _mapUnit(mapping.defaultUnit!);
|
||||
_unit = mapping.defaultUnit!;
|
||||
}
|
||||
if (mapping.storageDays != null) {
|
||||
_daysController.text = mapping.storageDays.toString();
|
||||
@@ -79,21 +77,6 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
String _mapUnit(String backendUnit) {
|
||||
switch (backendUnit.toLowerCase()) {
|
||||
case 'g':
|
||||
return 'г';
|
||||
case 'kg':
|
||||
return 'кг';
|
||||
case 'ml':
|
||||
return 'мл';
|
||||
case 'l':
|
||||
return 'л';
|
||||
default:
|
||||
return 'шт';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
final name = _nameController.text.trim();
|
||||
if (name.isEmpty) {
|
||||
@@ -170,7 +153,8 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
? Text(_categoryLabel(m.category!))
|
||||
: null,
|
||||
trailing: m.defaultUnit != null
|
||||
? Text(m.defaultUnit!,
|
||||
? Text(
|
||||
ref.watch(unitsProvider).valueOrNull?[m.defaultUnit!] ?? m.defaultUnit!,
|
||||
style:
|
||||
Theme.of(context).textTheme.bodySmall)
|
||||
: null,
|
||||
@@ -197,15 +181,18 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: _units.contains(_unit) ? _unit : _units.last,
|
||||
items: _units
|
||||
.map((u) =>
|
||||
DropdownMenuItem(value: u, child: Text(u)))
|
||||
.toList(),
|
||||
onChanged: (v) => setState(() => _unit = v!),
|
||||
ref.watch(unitsProvider).when(
|
||||
data: (units) => DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: units.containsKey(_unit) ? _unit : units.keys.first,
|
||||
items: units.entries
|
||||
.map((e) => DropdownMenuItem(value: e.key, child: Text(e.value)))
|
||||
.toList(),
|
||||
onChanged: (v) => setState(() => _unit = v!),
|
||||
),
|
||||
),
|
||||
loading: () => const SizedBox(width: 60, child: LinearProgressIndicator()),
|
||||
error: (_, __) => const Text('?'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../core/locale/unit_provider.dart';
|
||||
import '../../shared/models/product.dart';
|
||||
import 'product_provider.dart';
|
||||
|
||||
@@ -205,7 +206,7 @@ class _ProductTile extends ConsumerWidget {
|
||||
),
|
||||
title: Text(product.name),
|
||||
subtitle: Text(
|
||||
'${_formatQty(product.quantity)} ${product.unit}',
|
||||
'${_formatQty(product.quantity)} ${ref.watch(unitsProvider).valueOrNull?[product.unit] ?? product.unit}',
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
trailing: Column(
|
||||
@@ -303,8 +304,6 @@ class _EditProductSheetState extends ConsumerState<_EditProductSheet> {
|
||||
TextEditingController(text: widget.product.storageDays.toString());
|
||||
bool _saving = false;
|
||||
|
||||
static const _units = ['г', 'кг', 'мл', 'л', 'шт'];
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_qtyController.dispose();
|
||||
@@ -340,12 +339,16 @@ class _EditProductSheetState extends ConsumerState<_EditProductSheet> {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
DropdownButton<String>(
|
||||
value: _units.contains(_unit) ? _unit : _units.first,
|
||||
items: _units
|
||||
.map((u) => DropdownMenuItem(value: u, child: Text(u)))
|
||||
.toList(),
|
||||
onChanged: (v) => setState(() => _unit = v!),
|
||||
ref.watch(unitsProvider).when(
|
||||
data: (units) => DropdownButton<String>(
|
||||
value: units.containsKey(_unit) ? _unit : units.keys.first,
|
||||
items: units.entries
|
||||
.map((e) => DropdownMenuItem(value: e.key, child: Text(e.value)))
|
||||
.toList(),
|
||||
onChanged: (v) => setState(() => _unit = v!),
|
||||
),
|
||||
loading: () => const SizedBox(width: 60, child: LinearProgressIndicator()),
|
||||
error: (_, __) => const Text('?'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user