Adds the Flutter inventory app source, cleaned up for a shared repo: generic package name, no hardcoded ERP endpoints (moved to gitignored local config), no dead auth code, no debug logging of session data.
263 lines
7.1 KiB
Dart
263 lines
7.1 KiB
Dart
import 'dart:typed_data';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:inventory_app/constants.dart';
|
|
import 'package:inventory_app/models/famille_produit.dart';
|
|
import 'package:inventory_app/models/depot.dart';
|
|
import 'package:inventory_app/models/inventory_line.dart';
|
|
import 'package:inventory_app/models/product.dart';
|
|
import 'package:inventory_app/models/tracking_number.dart';
|
|
import 'package:inventory_app/service/axelor_client.dart';
|
|
import 'package:inventory_app/views/login_view.dart';
|
|
|
|
enum InventoryLineState { newLine, existingLine }
|
|
|
|
class HomeController extends GetxController {
|
|
final outputController = TextEditingController();
|
|
final labelController = TextEditingController();
|
|
final qtyController = TextEditingController(text: "1");
|
|
final unitController = TextEditingController();
|
|
final serialNumberTxt = TextEditingController();
|
|
|
|
var product = Rxn<Product>();
|
|
var selectedOption = 'comptage1'.obs;
|
|
|
|
var depotList = <Depot>[].obs;
|
|
var selectedDepot = Rxn<Depot>();
|
|
|
|
var trackingNumbers = <TrackingNumber>[].obs;
|
|
var selectedTrackingNumber = Rxn<TrackingNumber>();
|
|
|
|
var isLoading = false.obs;
|
|
var imageUrl = ''.obs;
|
|
|
|
var familleProduits = <FamilleProduit>[].obs;
|
|
var selectedFamilleProduit = Rxn<FamilleProduit>();
|
|
|
|
var sousFamilleProduits = <FamilleProduit>[].obs;
|
|
var selectedSousFamilleProduit = Rxn<FamilleProduit>();
|
|
|
|
var zoneCode = ''.obs;
|
|
var selectedState = 'Moyen'.obs;
|
|
|
|
late AxelorClient client;
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
_initClient();
|
|
}
|
|
|
|
@override
|
|
void onReady() {
|
|
super.onReady();
|
|
_handleArgs();
|
|
}
|
|
|
|
Future<void> _initClient() async {
|
|
isLoading(true);
|
|
client = await AxelorClient.create();
|
|
depotList.value = (await client.fetchLocations()) ?? [];
|
|
isLoading(false);
|
|
}
|
|
|
|
Future<void> fetchLocations() async {
|
|
isLoading(true);
|
|
client = await AxelorClient.create();
|
|
depotList.value = (await client.fetchLocations()) ?? [];
|
|
isLoading(false);
|
|
}
|
|
|
|
Future<void> _loadProduct(String code) async {
|
|
try {
|
|
isLoading(true);
|
|
|
|
Product? p = await client.fetchProductByCode(code);
|
|
if (p == null) {
|
|
Get.snackbar('Erreur', 'Produit introuvable', backgroundColor: Colors.red);
|
|
return;
|
|
}
|
|
|
|
product.value = p;
|
|
labelController.text = p.name ?? '';
|
|
unitController.text = p.unit?.name ?? '';
|
|
imageUrl.value = p.imageUrl ?? '';
|
|
|
|
trackingNumbers.value =
|
|
await client.fetchTrackingNumberByProduct(p.id!) ?? [];
|
|
|
|
Get.snackbar(
|
|
'Produit trouvé',
|
|
'Produit: ${p.name}',
|
|
backgroundColor: Colors.green,
|
|
duration: const Duration(seconds: 2),
|
|
);
|
|
} catch (e) {
|
|
Get.snackbar(
|
|
'Erreur',
|
|
'Impossible de charger le produit',
|
|
backgroundColor: Colors.red,
|
|
);
|
|
} finally {
|
|
isLoading(false);
|
|
}
|
|
}
|
|
|
|
Future<void> _handleArgs() async {
|
|
final args = Get.arguments;
|
|
if (args is String) {
|
|
outputController.text = args;
|
|
await _loadProduct(args);
|
|
}
|
|
}
|
|
|
|
void handleScannedCode(String code) async {
|
|
outputController.text = code;
|
|
trackingNumbers.clear();
|
|
selectedTrackingNumber.value = null;
|
|
await _loadProduct(code);
|
|
}
|
|
|
|
void handleProductScan(String code) => handleScannedCode(code);
|
|
|
|
void handleLocationScan(String name) async {
|
|
var finalStr = "";
|
|
if (name.isNotEmpty) {
|
|
finalStr = name.split(":")[0].trim();
|
|
}
|
|
|
|
zoneCode.value = finalStr;
|
|
final locationJson = await client.fetchStockLocationByName(name);
|
|
if (locationJson == null) {
|
|
Get.snackbar(
|
|
'Erreur',
|
|
'Zone introuvable (Veuillez rafraichir la page et ressayer)',
|
|
backgroundColor: Colors.red,
|
|
);
|
|
return;
|
|
}
|
|
|
|
final depot = Depot.fromJson(locationJson);
|
|
selectedDepot.value = depot;
|
|
|
|
Get.snackbar('Zone trouvée', 'Nom: ${depot.name}', backgroundColor: Colors.blue);
|
|
}
|
|
|
|
Future<void> save() async {
|
|
if (outputController.text.isEmpty ||
|
|
qtyController.text.isEmpty ||
|
|
product.value == null) {
|
|
Get.snackbar('Erreur', 'Produit ou quantité invalide', backgroundColor: Colors.red);
|
|
return;
|
|
}
|
|
|
|
final now = DateTime.now().toIso8601String();
|
|
final qty = double.tryParse(qtyController.text) ?? 1;
|
|
final user = await client.fetchUserProfile();
|
|
final selected = selectedOption.value;
|
|
|
|
double first = 0, second = 0, third = 0;
|
|
String? firstDate, secondDate, thirdDate;
|
|
Map<String, dynamic>? firstUser, secondUser, thirdUser;
|
|
|
|
switch (selected) {
|
|
case "comptage1":
|
|
first = qty;
|
|
firstDate = now;
|
|
firstUser = user;
|
|
break;
|
|
case "comptage2":
|
|
second = qty;
|
|
secondDate = now;
|
|
secondUser = user;
|
|
break;
|
|
case "comptage3":
|
|
third = qty;
|
|
thirdDate = now;
|
|
thirdUser = user;
|
|
break;
|
|
}
|
|
|
|
final line = InventoryLine(
|
|
inventoryId: kInventoryId,
|
|
productId: product.value!.id!,
|
|
productName: product.value!.name ?? '',
|
|
currentQty: qty,
|
|
realQty: qty,
|
|
unitId: product.value!.unit?.id ?? 2,
|
|
description: selectedState.value,
|
|
observation: serialNumberTxt.text,
|
|
ticketId: "TICKET-${DateTime.now().millisecondsSinceEpoch}",
|
|
rack: "A1",
|
|
trackingNumberId: selectedTrackingNumber.value?.id,
|
|
countingTypeSelect: 1,
|
|
stockLocationId: selectedDepot.value?.id ?? 4,
|
|
firstCounting: first,
|
|
secondCounting: second,
|
|
thirdCounting: third,
|
|
firstCountingDate: firstDate,
|
|
secondCountingDate: secondDate,
|
|
thirdCountingDate: thirdDate,
|
|
firstCountingByUser: firstUser,
|
|
secondCountingByUser: secondUser,
|
|
thirdCountingByUser: thirdUser,
|
|
);
|
|
|
|
await client.saveInventoryLine(inventoryLine: line);
|
|
|
|
_resetForm();
|
|
|
|
Get.snackbar('Succès', 'Ligne enregistrée avec succès', backgroundColor: Colors.green);
|
|
}
|
|
|
|
void _resetForm() {
|
|
outputController.clear();
|
|
labelController.clear();
|
|
product.value = null;
|
|
trackingNumbers.clear();
|
|
selectedTrackingNumber.value = null;
|
|
serialNumberTxt.text = "";
|
|
}
|
|
|
|
Future<Uint8List?> fetchImageBytes(int imageId) => client.fetchImageBytes(imageId);
|
|
|
|
Future<void> fetchFamilleProduits() async {
|
|
try {
|
|
isLoading(true);
|
|
final list = await client.getFamilleProduit();
|
|
familleProduits.value = list ?? [];
|
|
} catch (e) {
|
|
familleProduits.clear();
|
|
} finally {
|
|
isLoading(false);
|
|
}
|
|
}
|
|
|
|
Future<void> fetchSousFamilleProduits(int parentId) async {
|
|
try {
|
|
isLoading(true);
|
|
final list = await client.getSousFamilleProduit(parentId);
|
|
sousFamilleProduits.value = list ?? [];
|
|
} catch (e) {
|
|
sousFamilleProduits.clear();
|
|
} finally {
|
|
isLoading(false);
|
|
}
|
|
}
|
|
|
|
void onFamilleSelected(FamilleProduit? famille) async {
|
|
selectedFamilleProduit.value = famille;
|
|
selectedSousFamilleProduit.value = null;
|
|
|
|
if (famille?.id != null) {
|
|
await fetchSousFamilleProduits(famille!.id!);
|
|
} else {
|
|
sousFamilleProduits.clear();
|
|
}
|
|
}
|
|
|
|
void logout() async {
|
|
if (await client.logout()) Get.off(LoginView());
|
|
}
|
|
}
|