Files
ERP-APP-INVENTORY/lib/service/product_image_updater.dart
BACHIR SOULDI 5a483da01d Add full app source
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.
2026-07-16 14:37:28 +01:00

156 lines
4.3 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart' as d;
import 'package:flutter/material.dart';
import 'package:get_storage/get_storage.dart';
import 'package:image_picker/image_picker.dart';
import 'package:get/get.dart';
import 'package:inventory_app/models/product.dart';
import 'package:mime/mime.dart';
class ProductImageUpdater {
final d.Dio dio = d.Dio();
final String baseUrl;
final box = GetStorage();
ProductImageUpdater({required this.baseUrl});
final ImagePicker _picker = ImagePicker();
/// Step 1: Pick or take a photo
Future<File?> pickPhoto({bool fromCamera = true}) async {
final XFile? photo = await _picker.pickImage(
source: fromCamera ? ImageSource.camera : ImageSource.gallery,
);
return photo != null ? File(photo.path) : null;
}
Future<int?> uploadMetaFile(File file) async {
final sessionId = box.read('sessionId');
if (sessionId == null) {
Get.snackbar("Erreur", "Session ID introuvable",backgroundColor: Colors.red);
return null;
}
final fileName = file.path.split('/').last;
final fileSize = await file.length();
if (fileSize > 5 * 1024 * 1024) {
Get.snackbar("Erreur", "Fichier trop volumineux. Max: 5MB",backgroundColor: Colors.red);
return null;
}
final mimeType = lookupMimeType(file.path) ?? 'application/octet-stream';
try {
final response = await dio.post(
'$baseUrl/ws/files/upload',
data: file.openRead(), // ✅ Send binary stream
options: d.Options(
headers: {
'Cookie': 'JSESSIONID=$sessionId',
'Content-Type': 'application/octet-stream',
'X-File-Name': fileName,
'X-File-Size': fileSize.toString(),
'X-File-Type': mimeType,
'X-File-Offset': '0',
},
responseType: d.ResponseType.json,
),
);
print('📦 StatusCode: ${response.statusCode}');
print('📦 Response: ${response.data}');
if (response.statusCode == 200 && response != null) {
final int id = response.data['id'];
print('✅ File uploaded, MetaFile ID: $id');
return id;
} else {
print('❌ Upload failed: ${response.data}');
}
} on d.DioException catch (e) {
print('❌ DioException: ${e.message}');
if (e.response != null) {
print('📄 Response data: ${e.response?.data}');
}
} catch (e) {
print('❌ Unexpected error: $e');
}
return null;
}
/// Step 3: Update the product with the image ID
Future<bool> updateProductImage({
required Product product,
required int imageId,
}) async {
final sessionId = box.read('sessionId');
if (sessionId == null) {
Get.snackbar("Erreur", "Session ID introuvable",backgroundColor: Colors.red);
return false;
}
final payload = {
"data": {
"id": product.id,
"version": product.version,
"picture": {"id": imageId},
},
};
print('✅ payload $payload');
try {
final response = await dio.post(
'$baseUrl/ws/rest/com.axelor.apps.base.db.Product/${product.id}',
data: jsonEncode(payload),
options: d.Options(
headers: {
'Cookie': 'JSESSIONID=$sessionId',
'Content-Type': 'application/json',
},
),
);
if (response.statusCode == 200 && response.data['status'] == 0) {
print('✅ Product image updated');
return true;
} else {
print('❌ Update failed: ${response.data}');
}
} catch (e) {
print('❌ d.Dio error: $e');
}
return false;
}
/// Step 4: Combine all actions in one flow
Future<void> updateProductPictureFlow(
BuildContext context,
Product product,
) async {
File? file = await pickPhoto();
if (file == null) {
Get.snackbar("Annulé", "Aucune image sélectionnée",backgroundColor: Colors.red);
return;
}
final imageId = await uploadMetaFile(file);
print('✅ imageId $imageId');
if (imageId == null) {
Get.snackbar("Erreur", "Upload échoué",backgroundColor: Colors.red);
return;
}
final success = await updateProductImage(
product: product,
imageId: imageId,
);
if (success) {
Get.snackbar("Succès", "Image du produit mise à jour",backgroundColor: Colors.green);
}
}
}