Web approach - basic look and feel done
This commit is contained in:
@@ -85,7 +85,14 @@ class Product {
|
||||
0.0,
|
||||
isActive: json['isActive'] ?? json['is_active'] ?? true,
|
||||
imageIds: json['images'] != null
|
||||
? (json['images'] as List).map((i) => i['id'] as int).toList()
|
||||
? (json['images'] as List).map((i) {
|
||||
if (i is Map) {
|
||||
return (i['id'] as num).toInt();
|
||||
} else if (i is num) {
|
||||
return i.toInt();
|
||||
}
|
||||
return 0;
|
||||
}).where((id) => id > 0).toList()
|
||||
: [],
|
||||
currentStock:
|
||||
(json['currentStock'] ?? json['current_stock'] as num?)?.toDouble() ??
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart' as import_storage;
|
||||
import '../../../core/network/dio_client.dart';
|
||||
import '../../../../core/widgets/smart_search_dropdown.dart';
|
||||
import '../../../core/widgets/responsive_layout.dart';
|
||||
import '../domain/product.dart';
|
||||
import '../providers/products_provider.dart';
|
||||
import '../providers/product_categories_provider.dart';
|
||||
import '../providers/uoms_provider.dart';
|
||||
import '../../business/providers/business_provider.dart';
|
||||
import '../../transactions/presentation/widgets/attachment_gallery_screen.dart';
|
||||
|
||||
@@ -113,7 +116,11 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
}
|
||||
}
|
||||
for (final file in _images) {
|
||||
allImages.add(FileImage(File(file.path)));
|
||||
if (kIsWeb) {
|
||||
allImages.add(NetworkImage(file.path));
|
||||
} else {
|
||||
allImages.add(FileImage(File(file.path)));
|
||||
}
|
||||
}
|
||||
|
||||
Navigator.push(
|
||||
@@ -169,6 +176,7 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final categoriesState = ref.watch(productCategoriesProvider);
|
||||
final uomsState = ref.watch(uomsProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
@@ -177,9 +185,12 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
),
|
||||
body: GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Column(
|
||||
body: MaxContentWidth(
|
||||
maxWidth: 900,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Form(
|
||||
@@ -209,16 +220,21 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
const Text('Category & Unit', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 12),
|
||||
categoriesState.when(
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
error: (err, stack) => Text('Error loading categories: $err'),
|
||||
loading: () => const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
child: LinearProgressIndicator(),
|
||||
),
|
||||
error: (err, stack) => Text('Error loading categories: $err', style: const TextStyle(color: Colors.red)),
|
||||
data: (categories) {
|
||||
final leafCategories = categories.where((c) => !categories.any((child) => child.parentCategoryId == c.id)).toList();
|
||||
return leafCategories.isEmpty
|
||||
final activeCategories = categories.where((c) => c.isActive).toList();
|
||||
final uoms = uomsState.value ?? [];
|
||||
|
||||
return activeCategories.isEmpty
|
||||
? const Text('No categories found. Manage categories in Business Hub.', style: TextStyle(color: Colors.orange))
|
||||
: SmartSearchDropdown<ProductCategory>(
|
||||
hintText: 'Select Category*',
|
||||
value: _selectedCategory,
|
||||
items: leafCategories,
|
||||
items: activeCategories,
|
||||
itemAsString: (c) => c.name,
|
||||
itemBuilder: (context, item) {
|
||||
List<String> path = [];
|
||||
@@ -232,12 +248,13 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
}
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(item.name, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text(path.join(' -> '), style: TextStyle(fontSize: 12, color: Colors.grey.shade600)),
|
||||
if (path.length > 1)
|
||||
Text(path.join(' -> '), style: TextStyle(fontSize: 12, color: Colors.grey.shade600)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -258,6 +275,15 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
if (val.makingChargeType != null && val.makingChargeType!.isNotEmpty) {
|
||||
_makingChargesType = val.makingChargeType!;
|
||||
}
|
||||
if (_uomId == null && uoms.isNotEmpty) {
|
||||
final match = uoms.where((u) =>
|
||||
u.abbreviation?.toLowerCase() == val.baseUnit.toLowerCase() ||
|
||||
u.name.toLowerCase() == val.baseUnit.toLowerCase()
|
||||
).firstOrNull;
|
||||
if (match != null) {
|
||||
_uomId = match.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -265,15 +291,28 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<int>(
|
||||
decoration: const InputDecoration(labelText: 'Unit of Measure (Optional)'),
|
||||
value: _uomId,
|
||||
items: const [
|
||||
DropdownMenuItem(value: 1, child: Text('Grams (g)')),
|
||||
DropdownMenuItem(value: 2, child: Text('Kilograms (kg)')),
|
||||
DropdownMenuItem(value: 3, child: Text('Pieces (pcs)')),
|
||||
],
|
||||
onChanged: (val) => setState(() => _uomId = val),
|
||||
uomsState.when(
|
||||
loading: () => const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
child: LinearProgressIndicator(),
|
||||
),
|
||||
error: (err, stack) => Text('Error loading units: $err', style: const TextStyle(color: Colors.red)),
|
||||
data: (uoms) {
|
||||
final effectiveValue = uoms.any((u) => u.id == _uomId) ? _uomId : null;
|
||||
return DropdownButtonFormField<int>(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Unit of Measure (Optional)',
|
||||
),
|
||||
isExpanded: true,
|
||||
hint: const Text('None (Optional)'),
|
||||
value: effectiveValue,
|
||||
items: uoms.map((u) => DropdownMenuItem<int>(
|
||||
value: u.id,
|
||||
child: Text(u.displayName),
|
||||
)).toList(),
|
||||
onChanged: (val) => setState(() => _uomId = val),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
@@ -314,6 +353,7 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: _makingChargesType,
|
||||
isExpanded: true,
|
||||
decoration: const InputDecoration(labelText: 'Charge Type'),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'PER_GRAM', child: Text('Per Gram')),
|
||||
@@ -364,7 +404,7 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
final localIndex = index - _existingImageIds.length;
|
||||
return _buildImageThumbnail(
|
||||
isNetwork: false,
|
||||
file: File(_images[localIndex].path),
|
||||
xFile: _images[localIndex],
|
||||
onTap: () => _previewImage(index),
|
||||
onDelete: () => _removeImage(localIndex),
|
||||
);
|
||||
@@ -379,8 +419,8 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.05),
|
||||
border: Border.all(color: Theme.of(context).colorScheme.primary.withOpacity(0.3)),
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.05),
|
||||
border: Border.all(color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.3)),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
@@ -403,6 +443,7 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -428,7 +469,13 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImageThumbnail({required bool isNetwork, String? url, File? file, required VoidCallback onDelete, required VoidCallback onTap}) {
|
||||
Widget _buildImageThumbnail({
|
||||
required bool isNetwork,
|
||||
String? url,
|
||||
XFile? xFile,
|
||||
required VoidCallback onDelete,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
@@ -437,7 +484,7 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||
border: Border.all(color: Colors.grey.withValues(alpha: 0.2)),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
@@ -452,7 +499,9 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
headers: {'Authorization': 'Bearer $_token'},
|
||||
errorBuilder: (ctx, err, stack) => const Icon(LucideIcons.imageOff, color: Colors.grey),
|
||||
))
|
||||
: Image.file(file!, fit: BoxFit.cover),
|
||||
: (kIsWeb
|
||||
? Image.network(xFile!.path, fit: BoxFit.cover)
|
||||
: Image.file(File(xFile!.path), fit: BoxFit.cover)),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -4,7 +4,7 @@ import '../providers/product_categories_provider.dart';
|
||||
import '../../../core/theme/nature_colors.dart';
|
||||
import '../../business/providers/business_mode_provider.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'dart:ui';
|
||||
import '../../../core/utils/purity_utils.dart';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../providers/product_categories_provider.dart';
|
||||
import '../providers/commodity_rates_provider.dart';
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import '../providers/products_provider.dart';
|
||||
import '../providers/product_categories_provider.dart';
|
||||
import 'add_product_screen.dart';
|
||||
import 'product_detail_screen.dart';
|
||||
import '../../../core/network/dio_client.dart';
|
||||
import '../../../core/widgets/responsive_layout.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
class ProductListScreen extends ConsumerStatefulWidget {
|
||||
@@ -66,7 +67,9 @@ class _ProductListScreenState extends ConsumerState<ProductListScreen> {
|
||||
foregroundColor: Colors.black,
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Column(
|
||||
body: MaxContentWidth(
|
||||
maxWidth: 1000,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
color: Colors.white,
|
||||
@@ -138,13 +141,10 @@ class _ProductListScreenState extends ConsumerState<ProductListScreen> {
|
||||
}
|
||||
|
||||
final p = products[index];
|
||||
final catName =
|
||||
categoriesState.value
|
||||
?.firstWhere(
|
||||
(c) => c.id == p.categoryId,
|
||||
orElse: () => categoriesState.value!.first,
|
||||
)
|
||||
.name ??
|
||||
final catName = categoriesState.value
|
||||
?.where((c) => c.id == p.categoryId)
|
||||
.firstOrNull
|
||||
?.name ??
|
||||
'No Category';
|
||||
|
||||
return Container(
|
||||
@@ -267,12 +267,16 @@ class _ProductListScreenState extends ConsumerState<ProductListScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
onPressed: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const AddProductScreen()),
|
||||
);
|
||||
if (mounted) {
|
||||
ref.read(productsProvider.notifier).refresh();
|
||||
}
|
||||
},
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
child: const Icon(LucideIcons.plus, color: Colors.white),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:kifi_app/features/inventory/providers/products_provider.dart';
|
||||
import 'package:kifi_app/features/inventory/presentation/widgets/adjust_stock_sheet.dart';
|
||||
import 'package:kifi_app/core/network/dio_client.dart';
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:kifi_app/features/inventory/providers/product_categories_provide
|
||||
import 'package:kifi_app/features/inventory/providers/commodity_rates_provider.dart';
|
||||
import 'package:kifi_app/core/network/dio_client.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:kifi_app/features/vendor/presentation/purchase_order_details_screen.dart';
|
||||
import 'package:kifi_app/features/vendor/providers/purchase_orders_provider.dart';
|
||||
import 'package:kifi_app/core/utils/purity_utils.dart';
|
||||
|
||||
@@ -10,6 +10,7 @@ import '../domain/inventory_item.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class ProductsNotifier extends AsyncNotifier<List<Product>> {
|
||||
int _currentPage = 0;
|
||||
@@ -25,71 +26,74 @@ class ProductsNotifier extends AsyncNotifier<List<Product>> {
|
||||
return _fetchProducts(page: _currentPage, size: _pageSize);
|
||||
}
|
||||
|
||||
Future<List<Product>> _fetchProducts({
|
||||
required int page,
|
||||
required int size,
|
||||
}) async {
|
||||
final response = await DioClient().dio.get(
|
||||
'/inventory/products',
|
||||
queryParameters: {
|
||||
Future<List<Product>> _fetchProducts({int page = 0, int size = 50, String? search}) async {
|
||||
try {
|
||||
final queryParams = <String, dynamic>{
|
||||
'page': page,
|
||||
'size': size,
|
||||
if (_currentSearchQuery.isNotEmpty) 'search': _currentSearchQuery,
|
||||
},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = response.data;
|
||||
if (data.isNotEmpty) {
|
||||
try {
|
||||
final file = File(
|
||||
'/Users/maddy/Projects/Kifi/kifi-app/debug_product.json',
|
||||
);
|
||||
await file.writeAsString(jsonEncode(data.first));
|
||||
} catch (_) {}
|
||||
};
|
||||
if (search != null && search.isNotEmpty) {
|
||||
queryParams['search'] = search;
|
||||
}
|
||||
final products = data.map((e) => Product.fromJson(e)).toList();
|
||||
if (products.length < size) {
|
||||
_hasMore = false;
|
||||
|
||||
final response = await DioClient().dio.get(
|
||||
'/inventory/products',
|
||||
queryParameters: queryParams,
|
||||
);
|
||||
|
||||
if (response.data is List) {
|
||||
final List list = response.data;
|
||||
_hasMore = list.length >= size;
|
||||
return list.map((json) => Product.fromJson(json as Map<String, dynamic>)).toList();
|
||||
} else if (response.data is Map && response.data['content'] != null) {
|
||||
final List list = response.data['content'];
|
||||
_hasMore = !(response.data['last'] ?? true);
|
||||
return list.map((json) => Product.fromJson(json as Map<String, dynamic>)).toList();
|
||||
}
|
||||
return products;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
Future<void> fetchNextPage() async {
|
||||
if (!_hasMore || _isLoadingMore || state.isLoading) return;
|
||||
|
||||
_isLoadingMore = true;
|
||||
try {
|
||||
final currentList = state.value ?? [];
|
||||
final nextPage = _currentPage + 1;
|
||||
final newProducts = await _fetchProducts(page: nextPage, size: _pageSize);
|
||||
|
||||
_currentPage = nextPage;
|
||||
state = AsyncValue.data([...currentList, ...newProducts]);
|
||||
} catch (e, stack) {
|
||||
// Don't override state with error, just keep the current list, but maybe show a toast.
|
||||
} finally {
|
||||
_isLoadingMore = false;
|
||||
return [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
state = const AsyncValue.loading();
|
||||
_currentPage = 0;
|
||||
_hasMore = true;
|
||||
try {
|
||||
state = AsyncValue.data(
|
||||
await _fetchProducts(page: _currentPage, size: _pageSize),
|
||||
);
|
||||
} catch (e, stack) {
|
||||
state = AsyncValue.error(e, stack);
|
||||
}
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() => _fetchProducts(page: 0, size: _pageSize, search: _currentSearchQuery));
|
||||
}
|
||||
|
||||
Future<void> search(String query) async {
|
||||
_currentSearchQuery = query;
|
||||
await refresh();
|
||||
_currentPage = 0;
|
||||
_hasMore = true;
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() => _fetchProducts(page: 0, size: _pageSize, search: query));
|
||||
}
|
||||
|
||||
Future<void> fetchNextPage() async {
|
||||
await loadMore();
|
||||
}
|
||||
|
||||
Future<void> loadMore() async {
|
||||
if (!_hasMore || _isLoadingMore || state.isLoading) return;
|
||||
|
||||
_isLoadingMore = true;
|
||||
try {
|
||||
final nextPage = _currentPage + 1;
|
||||
final newItems = await _fetchProducts(page: nextPage, size: _pageSize, search: _currentSearchQuery);
|
||||
|
||||
if (newItems.isNotEmpty) {
|
||||
_currentPage = nextPage;
|
||||
state = AsyncValue.data([...state.value ?? [], ...newItems]);
|
||||
} else {
|
||||
_hasMore = false;
|
||||
}
|
||||
} catch (e) {
|
||||
// Handle silently or notify
|
||||
} finally {
|
||||
_isLoadingMore = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createProduct(Product product, {List<XFile>? images}) async {
|
||||
@@ -103,15 +107,20 @@ class ProductsNotifier extends AsyncNotifier<List<Product>> {
|
||||
final productId = response.data['id'];
|
||||
for (var image in images) {
|
||||
final bytes = await image.readAsBytes();
|
||||
final compressedBytes = await FlutterImageCompress.compressWithList(
|
||||
bytes,
|
||||
minWidth: 800,
|
||||
quality: 80,
|
||||
);
|
||||
Uint8List uploadBytes = bytes;
|
||||
if (!kIsWeb) {
|
||||
try {
|
||||
uploadBytes = await FlutterImageCompress.compressWithList(
|
||||
bytes,
|
||||
minWidth: 800,
|
||||
quality: 80,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
final formData = FormData.fromMap({
|
||||
'file': MultipartFile.fromBytes(
|
||||
compressedBytes,
|
||||
uploadBytes,
|
||||
filename: image.name,
|
||||
),
|
||||
});
|
||||
@@ -143,15 +152,20 @@ class ProductsNotifier extends AsyncNotifier<List<Product>> {
|
||||
if (newImages != null && newImages.isNotEmpty) {
|
||||
for (var image in newImages) {
|
||||
final bytes = await image.readAsBytes();
|
||||
final compressedBytes = await FlutterImageCompress.compressWithList(
|
||||
bytes,
|
||||
minWidth: 800,
|
||||
quality: 80,
|
||||
);
|
||||
Uint8List uploadBytes = bytes;
|
||||
if (!kIsWeb) {
|
||||
try {
|
||||
uploadBytes = await FlutterImageCompress.compressWithList(
|
||||
bytes,
|
||||
minWidth: 800,
|
||||
quality: 80,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
final formData = FormData.fromMap({
|
||||
'file': MultipartFile.fromBytes(
|
||||
compressedBytes,
|
||||
uploadBytes,
|
||||
filename: image.name,
|
||||
),
|
||||
});
|
||||
|
||||
102
kifi-app/lib/features/inventory/providers/uoms_provider.dart
Normal file
102
kifi-app/lib/features/inventory/providers/uoms_provider.dart
Normal file
@@ -0,0 +1,102 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../core/network/dio_client.dart';
|
||||
|
||||
class UnitOfMeasure {
|
||||
final int? id;
|
||||
final int? userId;
|
||||
final String name;
|
||||
final String? abbreviation;
|
||||
|
||||
UnitOfMeasure({
|
||||
this.id,
|
||||
this.userId,
|
||||
required this.name,
|
||||
this.abbreviation,
|
||||
});
|
||||
|
||||
factory UnitOfMeasure.fromJson(Map<String, dynamic> json) {
|
||||
return UnitOfMeasure(
|
||||
id: json['id'],
|
||||
userId: json['userId'] ?? json['user_id'],
|
||||
name: json['name'] ?? '',
|
||||
abbreviation: json['abbreviation'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'userId': userId,
|
||||
'name': name,
|
||||
'abbreviation': abbreviation,
|
||||
};
|
||||
}
|
||||
|
||||
String get displayName => abbreviation != null && abbreviation!.isNotEmpty ? '$name ($abbreviation)' : name;
|
||||
}
|
||||
|
||||
class UomsNotifier extends AsyncNotifier<List<UnitOfMeasure>> {
|
||||
@override
|
||||
FutureOr<List<UnitOfMeasure>> build() async {
|
||||
return _fetchUoms();
|
||||
}
|
||||
|
||||
Future<List<UnitOfMeasure>> _fetchUoms() async {
|
||||
try {
|
||||
final response = await DioClient().dio.get('/inventory/uom');
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = response.data;
|
||||
final list = data.map((e) => UnitOfMeasure.fromJson(e)).toList();
|
||||
if (list.isNotEmpty) {
|
||||
return list;
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// If empty in database, seed standard units of measure
|
||||
try {
|
||||
final defaults = [
|
||||
{'name': 'Grams', 'abbreviation': 'g'},
|
||||
{'name': 'Kilograms', 'abbreviation': 'kg'},
|
||||
{'name': 'Pieces', 'abbreviation': 'pcs'},
|
||||
{'name': 'Carats', 'abbreviation': 'ct'},
|
||||
{'name': 'Milligrams', 'abbreviation': 'mg'},
|
||||
];
|
||||
for (final def in defaults) {
|
||||
await DioClient().dio.post('/inventory/uom', data: def);
|
||||
}
|
||||
final response = await DioClient().dio.get('/inventory/uom');
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = response.data;
|
||||
return data.map((e) => UnitOfMeasure.fromJson(e)).toList();
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() => _fetchUoms());
|
||||
}
|
||||
|
||||
Future<UnitOfMeasure?> createUom(String name, String abbreviation) async {
|
||||
try {
|
||||
final response = await DioClient().dio.post(
|
||||
'/inventory/uom',
|
||||
data: {'name': name, 'abbreviation': abbreviation},
|
||||
);
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
final newUom = UnitOfMeasure.fromJson(response.data);
|
||||
await refresh();
|
||||
return newUom;
|
||||
}
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
final uomsProvider = AsyncNotifierProvider<UomsNotifier, List<UnitOfMeasure>>(() {
|
||||
return UomsNotifier();
|
||||
});
|
||||
Reference in New Issue
Block a user