Add equipment management features (and qr generation support)

This commit is contained in:
ElPoyo
2025-10-21 16:32:18 +02:00
parent ef638d8c8c
commit ae3a1b7227
18 changed files with 4489 additions and 7 deletions

View File

@@ -0,0 +1,133 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:em2rp/providers/equipment_provider.dart';
class BrandModelSelector extends StatefulWidget {
final TextEditingController brandController;
final TextEditingController modelController;
final ValueChanged<String?>? onBrandChanged;
final List<String> filteredModels;
final String? selectedBrand;
final Function(List<String>) onModelsChanged;
const BrandModelSelector({
super.key,
required this.brandController,
required this.modelController,
this.onBrandChanged,
required this.filteredModels,
required this.selectedBrand,
required this.onModelsChanged,
});
@override
State<BrandModelSelector> createState() => _BrandModelSelectorState();
}
class _BrandModelSelectorState extends State<BrandModelSelector> {
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: Consumer<EquipmentProvider>(
builder: (context, provider, child) {
return Autocomplete<String>(
initialValue: TextEditingValue(text: widget.brandController.text),
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text.isEmpty) {
return provider.brands;
}
return provider.brands.where((String brand) {
return brand.toLowerCase().contains(
textEditingValue.text.toLowerCase(),
);
});
},
onSelected: (String selection) async {
widget.brandController.text = selection;
widget.onBrandChanged?.call(selection);
final equipmentProvider = Provider.of<EquipmentProvider>(context, listen: false);
final models = await equipmentProvider.loadModelsByBrand(selection);
widget.onModelsChanged(models);
},
fieldViewBuilder: (context, controller, focusNode, onEditingComplete) {
if (controller.text != widget.brandController.text) {
controller.text = widget.brandController.text;
}
return TextFormField(
controller: controller,
focusNode: focusNode,
decoration: const InputDecoration(
labelText: 'Marque *',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.business),
helperText: 'Champ obligatoire',
),
onChanged: (value) async {
widget.brandController.text = value;
widget.modelController.clear();
widget.onBrandChanged?.call(value.isNotEmpty ? value : null);
if (value.isNotEmpty) {
final equipmentProvider = Provider.of<EquipmentProvider>(context, listen: false);
final models = await equipmentProvider.loadModelsByBrand(value);
widget.onModelsChanged(models);
} else {
widget.onModelsChanged([]);
}
},
);
},
);
},
),
),
const SizedBox(width: 16),
Expanded(
child: Autocomplete<String>(
initialValue: TextEditingValue(text: widget.modelController.text),
optionsBuilder: (TextEditingValue textEditingValue) {
if (widget.selectedBrand == null || widget.selectedBrand!.isEmpty) {
return const Iterable<String>.empty();
}
if (textEditingValue.text.isEmpty) {
return widget.filteredModels;
}
return widget.filteredModels.where((String model) {
return model.toLowerCase().contains(
textEditingValue.text.toLowerCase(),
);
});
},
onSelected: (String selection) {
widget.modelController.text = selection;
},
fieldViewBuilder: (context, controller, focusNode, onEditingComplete) {
if (controller.text != widget.modelController.text) {
controller.text = widget.modelController.text;
}
return TextFormField(
controller: controller,
focusNode: focusNode,
enabled: widget.selectedBrand != null && widget.selectedBrand!.isNotEmpty,
decoration: InputDecoration(
labelText: 'Modèle *',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.inventory_2),
hintText: widget.selectedBrand == null || widget.selectedBrand!.isEmpty
? 'Marque requise'
: 'Saisissez le modèle',
helperText: 'Champ obligatoire',
),
onChanged: (value) {
widget.modelController.text = value;
},
);
},
),
),
],
);
}
}

View File

@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:em2rp/services/equipment_service.dart';
class EquipmentIdGenerator {
static String generate({required String brand, required String model, int? number}) {
final brandTrim = brand.trim().replaceAll(' ', '_');
final modelTrim = model.trim().replaceAll(' ', '_');
if (brandTrim.isEmpty && modelTrim.isEmpty) {
return 'EQ-${DateTime.now().millisecondsSinceEpoch}${number != null ? '_$number' : ''}';
}
final brandPrefix = brandTrim.length >= 4 ? brandTrim.substring(0, 4) : brandTrim;
String baseId = modelTrim.isNotEmpty ? '${brandPrefix}_$modelTrim' : (brandPrefix.isNotEmpty ? brandPrefix : 'EQ');
if (number != null) {
baseId += '_#$number';
}
return baseId;
}
static Future<String> ensureUniqueId(String baseId, EquipmentService service) async {
if (await service.isIdUnique(baseId)) {
return baseId;
}
return '${baseId}_${DateTime.now().millisecondsSinceEpoch}';
}
}