Web approach - basic look and feel done

This commit is contained in:
2026-08-29 21:52:11 +05:30
parent 364961c9de
commit 372c2bc14d
71 changed files with 2209 additions and 1044 deletions

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/auth_provider.dart';

View File

@@ -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 '../providers/auth_provider.dart';
import 'reset_password_screen.dart';

View File

@@ -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 '../providers/auth_provider.dart';
import '../../dashboard/presentation/dashboard_screen.dart';
import 'setup_wizard_screen.dart';

View File

@@ -3,7 +3,7 @@ import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:cross_file/cross_file.dart';

View File

@@ -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 '../providers/auth_provider.dart';
import 'auth_screen.dart';

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:intl_phone_field/intl_phone_field.dart';
import '../../../../core/network/dio_client.dart';
import '../../dashboard/presentation/dashboard_screen.dart';

View File

@@ -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 '../../transactions/providers/providers.dart';
import '../../transactions/data/models.dart';
import '../../../core/widgets/shimmer_loading.dart';

View File

@@ -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 '../../../../core/theme/nature_colors.dart';
import '../../../inventory/presentation/product_list_screen.dart';
import '../../../inventory/presentation/daily_rates_screen.dart';

View File

@@ -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 '../../../inventory/providers/inventory_valuation_provider.dart';

View File

@@ -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 '../../providers/business_provider.dart';
class BusinessSettingsScreen extends ConsumerStatefulWidget {

View File

@@ -1,11 +1,12 @@
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 '../../transactions/providers/providers.dart';
import '../../transactions/data/models.dart';
import '../../transactions/data/repository.dart';
import 'wallet_ledger_screen.dart';
import '../../../core/widgets/responsive_layout.dart';
class AccountsScreen extends ConsumerStatefulWidget {
final String? initialFilterNature;
@@ -449,9 +450,11 @@ class _AccountsScreenState extends ConsumerState<AccountsScreen> {
final walletsState = ref.watch(walletProvider);
return Scaffold(
body: SafeArea(
bottom: false,
child: Column(
body: MaxContentWidth(
maxWidth: 1200,
child: SafeArea(
bottom: false,
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
@@ -939,6 +942,7 @@ class _AccountsScreenState extends ConsumerState<AccountsScreen> {
],
),
),
),
);
}
}

View File

@@ -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 '../../transactions/data/models.dart';
import '../../transactions/providers/providers.dart';

View File

@@ -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 '../../../transactions/providers/providers.dart';
class BudgetStatusCard extends ConsumerWidget {

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../../transactions/data/models.dart';
import '../../../../core/theme/nature_colors.dart';
import 'day_wise_spending_chart.dart';

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
class CarouselItemData {
final String title;

View File

@@ -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 '../../../transactions/providers/providers.dart';
import '../../../transactions/data/models.dart';

View File

@@ -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() ??

View File

@@ -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)),
),
),
),

View File

@@ -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';

View File

@@ -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';

View File

@@ -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),

View File

@@ -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';

View File

@@ -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';

View File

@@ -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,
),
});

View 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();
});

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../dashboard/presentation/dashboard_screen.dart';
class OnboardingScreen extends StatefulWidget {

View File

@@ -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 '../domain/project.dart';
import '../providers/project_provider.dart';
import 'widgets/add_task_sheet.dart';

View File

@@ -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 '../../../../core/theme/nature_colors.dart';
import 'projects_screen.dart';
import '../../sales/presentation/invoices_list_screen.dart';

View File

@@ -1,4 +1,4 @@
import 'package:lucide_icons/lucide_icons.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/project_provider.dart';

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import '../../domain/project.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../../sales/presentation/add_customer_sheet.dart';
import '../../providers/project_provider.dart';
import '../../../sales/providers/customers_provider.dart';

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import '../../../../core/widgets/premium_text_field.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../domain/project_task.dart';
import '../../providers/project_provider.dart';
import '../../providers/user_provider.dart';

View File

@@ -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 '../../domain/project_task.dart';
import '../../providers/project_provider.dart';
import 'task_details_sheet.dart';

View File

@@ -2,7 +2,7 @@ import 'dart:convert';
import 'dart:io';
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:file_picker/file_picker.dart';
import 'package:path_provider/path_provider.dart';

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../providers/customers_provider.dart';
import '../domain/customer.dart';
import 'dart:async';
@@ -222,7 +223,7 @@ class _AddCustomerSheetState extends ConsumerState<AddCustomerSheet> {
radius: 50,
backgroundColor: Colors.grey[200],
backgroundImage: _photo != null
? FileImage(File(_photo!.path)) as ImageProvider
? (kIsWeb ? NetworkImage(_photo!.path) : FileImage(File(_photo!.path))) as ImageProvider
: (widget.customer?.photoUrl != null &&
_token != null
? NetworkImage(

View File

@@ -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 '../providers/customers_provider.dart';
import 'dart:async';
import '../../../core/widgets/shimmer_loading.dart';

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.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 'dart:io';
import 'package:dio/dio.dart';
@@ -710,7 +711,7 @@ class _InvoiceBuilderScreenState extends ConsumerState<InvoiceBuilderScreen> {
context,
MaterialPageRoute(
builder: (_) => AttachmentGalleryScreen(
images: [FileImage(File(_invoiceFile!.path))],
images: [kIsWeb ? NetworkImage(_invoiceFile!.path) : FileImage(File(_invoiceFile!.path))],
initialIndex: 0,
),
),

View File

@@ -2,7 +2,7 @@ import 'dart:io';
import 'dart:typed_data';
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 'package:screenshot/screenshot.dart';
import 'package:path_provider/path_provider.dart';

View File

@@ -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/invoices_provider.dart';
import '../domain/invoice.dart';

View File

@@ -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 '../../../../core/widgets/premium_text_field.dart';
import '../../../../core/widgets/smart_search_dropdown.dart';
import '../../providers/customers_provider.dart';

View File

@@ -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 '../../../../core/widgets/premium_text_field.dart';
import '../../../transactions/providers/providers.dart';
import '../../providers/invoices_provider.dart';

View File

@@ -1,5 +1,7 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:dio/dio.dart';
import 'package:image_picker/image_picker.dart';
@@ -90,15 +92,20 @@ class CustomersNotifier extends AsyncNotifier<List<Customer>> {
Future<void> _uploadPhoto(int customerId, XFile photo) async {
final bytes = await photo.readAsBytes();
final compressedBytes = await FlutterImageCompress.compressWithList(
bytes,
minWidth: 413,
minHeight: 531,
quality: 85,
);
Uint8List uploadBytes = bytes;
if (!kIsWeb) {
try {
uploadBytes = await FlutterImageCompress.compressWithList(
bytes,
minWidth: 413,
minHeight: 531,
quality: 85,
);
} catch (_) {}
}
final formData = FormData.fromMap({
'file': MultipartFile.fromBytes(compressedBytes, filename: photo.name),
'file': MultipartFile.fromBytes(uploadBytes, filename: photo.name),
});
await DioClient().dio.post('/customers/$customerId/photo', data: formData);

View File

@@ -1,12 +1,16 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'widgets/attachment_gallery_screen.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 'package:flutter_secure_storage/flutter_secure_storage.dart' as flutter_secure_storage;
import 'package:image_picker/image_picker.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import '../../../core/network/dio_client.dart';
import '../../../core/widgets/responsive_layout.dart';
import '../providers/providers.dart';
import '../data/models.dart';
import '../../../core/utils/snackbar_service.dart';
@@ -133,11 +137,16 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
final XFile? image = await _picker.pickImage(source: source);
if (image != null) {
final bytes = await image.readAsBytes();
final compressedBytes = await FlutterImageCompress.compressWithList(
bytes,
minWidth: 600,
quality: 80,
);
Uint8List compressedBytes = bytes;
if (!kIsWeb) {
try {
compressedBytes = await FlutterImageCompress.compressWithList(
bytes,
minWidth: 600,
quality: 80,
);
} catch (_) {}
}
final base64String = base64Encode(compressedBytes);
setState(() {
base64Attachments.add({
@@ -751,8 +760,10 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
)
],
),
body: SafeArea(
child: SingleChildScrollView(
body: MaxContentWidth(
maxWidth: 800,
child: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -988,7 +999,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: _jwtToken != null
? NetworkImage('https://app.technobeesolutions.in/api/kifi-v2/transactions/${widget.transaction!.id}/attachments/${att.id}/content', headers: {'Authorization': 'Bearer $_jwtToken'})
? NetworkImage('${DioClient().dio.options.baseUrl}/transactions/${widget.transaction!.id}/attachments/${att.id}/content', headers: {'Authorization': 'Bearer $_jwtToken'})
: const AssetImage('assets/images/placeholder.png') as ImageProvider,
fit: BoxFit.cover,
),
@@ -1096,6 +1107,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
),
),
),
),
);
}
@@ -1105,7 +1117,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
// Add existing attachments
for (final att in existingAttachments) {
if (_jwtToken != null) {
allImages.add(NetworkImage('https://app.technobeesolutions.in/api/kifi-v2/transactions/${widget.transaction!.id}/attachments/${att.id}/content', headers: {'Authorization': 'Bearer $_jwtToken'}));
allImages.add(NetworkImage('${DioClient().dio.options.baseUrl}/transactions/${widget.transaction!.id}/attachments/${att.id}/content', headers: {'Authorization': 'Bearer $_jwtToken'}));
} else {
allImages.add(const AssetImage('assets/images/placeholder.png'));
}

View File

@@ -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/providers.dart';
import '../../dashboard/presentation/maturity_dialog.dart';
@@ -10,6 +10,7 @@ import '../../../core/widgets/empty_state.dart';
import 'add_transaction_screen.dart';
import '../presentation/widgets/transaction_filter_sheet.dart';
import '../../../core/theme/nature_colors.dart';
import '../../../core/widgets/responsive_layout.dart';
class AllTransactionsScreen extends ConsumerStatefulWidget {
const AllTransactionsScreen({super.key});
@@ -40,7 +41,6 @@ class _AllTransactionsScreenState extends ConsumerState<AllTransactionsScreen> {
}
void _onSearchChanged(String value) {
// Basic debounce for text search
Future.delayed(const Duration(milliseconds: 500), () {
if (_searchController.text == value) {
ref.read(paginatedTransactionProvider.notifier).updateFilters(search: value);
@@ -88,13 +88,15 @@ class _AllTransactionsScreenState extends ConsumerState<AllTransactionsScreen> {
decoration: InputDecoration(
hintText: 'Search transactions...',
prefixIcon: const Icon(LucideIcons.search),
suffixIcon: _searchController.text.isNotEmpty ? IconButton(
icon: const Icon(LucideIcons.x),
onPressed: () {
_searchController.clear();
_onSearchChanged('');
},
) : null,
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(LucideIcons.x),
onPressed: () {
_searchController.clear();
_onSearchChanged('');
},
)
: null,
filled: true,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(
@@ -102,8 +104,8 @@ class _AllTransactionsScreenState extends ConsumerState<AllTransactionsScreen> {
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2)
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
@@ -111,197 +113,196 @@ class _AllTransactionsScreenState extends ConsumerState<AllTransactionsScreen> {
),
),
),
body: Builder(
builder: (context) {
if (transactions.isEmpty && paginatedState.isLoading) {
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: 8,
itemBuilder: (context, index) => const ShimmerCard(),
);
}
body: MaxContentWidth(
maxWidth: 1200,
child: Builder(
builder: (context) {
if (transactions.isEmpty && paginatedState.isLoading) {
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: 8,
itemBuilder: (context, index) => const ShimmerCard(),
);
}
if (transactions.isEmpty) {
return const EmptyStateWidget(
icon: LucideIcons.fileText,
title: 'No Transactions',
message: 'No transactions found matching your criteria.',
);
}
if (transactions.isEmpty) {
return const EmptyStateWidget(
icon: LucideIcons.fileText,
title: 'No Transactions',
message: 'No transactions found matching your criteria.',
);
}
return RefreshIndicator(
onRefresh: () async {
ref.read(paginatedTransactionProvider.notifier).clearFilters();
_searchController.clear();
},
child: ListView.builder(
controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16.0),
itemCount: transactions.length + (paginatedState.hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index == transactions.length) {
return const Padding(
padding: EdgeInsets.all(16.0),
child: Center(child: CircularProgressIndicator()),
);
}
return RefreshIndicator(
onRefresh: () async {
ref.read(paginatedTransactionProvider.notifier).clearFilters();
_searchController.clear();
},
child: ListView.builder(
controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16.0),
itemCount: transactions.length + (paginatedState.hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index == transactions.length) {
return const Padding(
padding: EdgeInsets.all(16.0),
child: Center(child: CircularProgressIndicator()),
);
}
final t = transactions[index];
final isIncome = t.type == 'INCOME' || (t.type == 'TRANSFER' && t.fromWalletId == null);
final isInvestment = t.type == 'INVESTMENT';
final isExpense = t.type == 'EXPENSE' || (t.type == 'TRANSFER' && t.toWalletId == null);
final isTransfer = t.type == 'TRANSFER' && t.fromWalletId != null && t.toWalletId != null;
Color typeColor = Colors.grey;
IconData typeIcon = LucideIcons.arrowRightLeft;
final t = transactions[index];
final isIncome = t.type == 'INCOME' || (t.type == 'TRANSFER' && t.fromWalletId == null);
final isInvestment = t.type == 'INVESTMENT';
final isExpense = t.type == 'EXPENSE' || (t.type == 'TRANSFER' && t.toWalletId == null);
final isTransfer = t.type == 'TRANSFER' && t.fromWalletId != null && t.toWalletId != null;
if (isIncome) {
typeColor = NatureColors.getColor('INCOME');
typeIcon = LucideIcons.arrowDownCircle;
} else if (isExpense) {
typeColor = NatureColors.getColor('EXPENSE');
typeIcon = LucideIcons.arrowUpCircle;
} else if (isInvestment) {
typeColor = NatureColors.getColor('INVESTMENTS');
typeIcon = LucideIcons.trendingUp;
} else if (isTransfer) {
typeColor = NatureColors.getColor('TRANSFER');
if (walletsState.hasValue) {
final toW = walletsState.value!.where((w) => w.id == t.toWalletId).firstOrNull;
final fromW = walletsState.value!.where((w) => w.id == t.fromWalletId).firstOrNull;
if (toW != null && (toW.nature == 'PAYABLES' || toW.nature == 'LOAN')) {
typeColor = NatureColors.getColor('PAYABLES');
typeIcon = LucideIcons.alertCircle;
} else if (toW != null && (toW.nature == 'RECEIVABLES' || toW.nature == 'LENDING')) {
typeColor = NatureColors.getColor('RECEIVABLES');
typeIcon = LucideIcons.arrowDownLeft;
} else if (fromW != null && (fromW.nature == 'PAYABLES' || fromW.nature == 'LOAN')) {
typeColor = NatureColors.getColor('PAYABLES');
typeIcon = LucideIcons.alertCircle;
} else if (fromW != null && (fromW.nature == 'RECEIVABLES' || fromW.nature == 'LENDING')) {
typeColor = NatureColors.getColor('RECEIVABLES');
typeIcon = LucideIcons.arrowDownLeft;
Color typeColor = Colors.grey;
IconData typeIcon = LucideIcons.arrowRightLeft;
if (isIncome) {
typeColor = NatureColors.getColor('INCOME');
typeIcon = LucideIcons.arrowDownCircle;
} else if (isExpense) {
typeColor = NatureColors.getColor('EXPENSE');
typeIcon = LucideIcons.arrowUpCircle;
} else if (isInvestment) {
typeColor = NatureColors.getColor('INVESTMENTS');
typeIcon = LucideIcons.trendingUp;
} else if (isTransfer) {
typeColor = NatureColors.getColor('TRANSFER');
if (walletsState.hasValue) {
final toW = walletsState.value!.where((w) => w.id == t.toWalletId).firstOrNull;
final fromW = walletsState.value!.where((w) => w.id == t.fromWalletId).firstOrNull;
if (toW != null && (toW.nature == 'PAYABLES' || toW.nature == 'LOAN')) {
typeColor = NatureColors.getColor('PAYABLES');
typeIcon = LucideIcons.alertCircle;
} else if (toW != null && (toW.nature == 'RECEIVABLES' || toW.nature == 'LENDING')) {
typeColor = NatureColors.getColor('RECEIVABLES');
typeIcon = LucideIcons.arrowDownLeft;
} else if (fromW != null && (fromW.nature == 'PAYABLES' || fromW.nature == 'LOAN')) {
typeColor = NatureColors.getColor('PAYABLES');
typeIcon = LucideIcons.alertCircle;
} else if (fromW != null && (fromW.nature == 'RECEIVABLES' || fromW.nature == 'LENDING')) {
typeColor = NatureColors.getColor('RECEIVABLES');
typeIcon = LucideIcons.arrowDownLeft;
}
}
}
}
String categoryName = 'Unknown';
if (categoriesState.hasValue) {
final match = categoriesState.value!.where((c) => c.id == t.categoryId);
if (match.isNotEmpty) categoryName = match.first.name;
}
String accountName = 'Unknown';
if (walletsState.hasValue) {
if (isExpense && t.toWalletId != null) {
final match = walletsState.value!.where((w) => w.id == t.toWalletId);
if (match.isNotEmpty) accountName = match.first.name;
} else if (isIncome && t.toWalletId != null) {
final match = walletsState.value!.where((w) => w.id == t.toWalletId);
if (match.isNotEmpty) accountName = match.first.name;
} else if (isTransfer && t.toWalletId != null) {
final match = walletsState.value!.where((w) => w.id == t.toWalletId);
if (match.isNotEmpty) accountName = 'To ${match.first.name}';
String categoryName = 'Unknown';
if (categoriesState.hasValue) {
final match = categoriesState.value!.where((c) => c.id == t.categoryId);
if (match.isNotEmpty) categoryName = match.first.name;
}
}
String fromName = '';
if (t.fromWalletId != null && walletsState.hasValue) {
final match = walletsState.value!.where((w) => w.id == t.fromWalletId);
if (match.isNotEmpty) fromName = match.first.name;
}
String toName = '';
if (t.toWalletId != null && walletsState.hasValue) {
final match = walletsState.value!.where((w) => w.id == t.toWalletId);
if (match.isNotEmpty) toName = match.first.name;
}
String subtitleText = '';
if (categoryName == 'Unknown' && fromName.isNotEmpty && toName.isNotEmpty) {
subtitleText = '$fromName$toName${DateFormat('MMM dd, yyyy').format(t.date)}';
} else if (categoryName == 'Unknown' && toName.isNotEmpty) {
subtitleText = 'To $toName${DateFormat('MMM dd, yyyy').format(t.date)}';
} else if (categoryName == 'Unknown' && fromName.isNotEmpty) {
subtitleText = 'From $fromName${DateFormat('MMM dd, yyyy').format(t.date)}';
} else {
subtitleText = '$categoryName${DateFormat('MMM dd, yyyy').format(t.date)}${fromName.isNotEmpty ? ' • 💼 $fromName' : ''}';
}
String accountName = 'Unknown';
if (walletsState.hasValue) {
if (isExpense && t.toWalletId != null) {
final match = walletsState.value!.where((w) => w.id == t.toWalletId);
if (match.isNotEmpty) accountName = match.first.name;
} else if (isIncome && t.toWalletId != null) {
final match = walletsState.value!.where((w) => w.id == t.toWalletId);
if (match.isNotEmpty) accountName = match.first.name;
} else if (isTransfer && t.toWalletId != null) {
final match = walletsState.value!.where((w) => w.id == t.toWalletId);
if (match.isNotEmpty) accountName = 'To ${match.first.name}';
}
}
return Dismissible(
key: Key(t.id.toString()),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
child: const Icon(LucideIcons.trash2, color: Colors.white),
),
confirmDismiss: (dir) async {
return await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Delete Transaction?'),
content: const Text('Are you sure you want to delete this transaction?'),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel')),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('Delete', style: TextStyle(color: Colors.red))
),
],
),
);
},
onDismissed: (dir) {
ref.read(transactionProvider.notifier).deleteTransaction(t.id);
ref.read(paginatedTransactionProvider.notifier).removeTransactionFromState(t.id);
},
child: Card(
margin: const EdgeInsets.only(bottom: 12),
child: Column(
children: [
ListTile(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => AddTransactionScreen(transaction: t)));
},
leading: CircleAvatar(
backgroundColor: typeColor.withValues(alpha: 0.1),
child: Icon(typeIcon, color: typeColor),
),
title: Text((t.description != null && t.description!.isNotEmpty) ? t.description! : accountName, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(subtitleText),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${isIncome ? '+' : (isTransfer ? '' : '-')}Rs. ${t.amount.toStringAsFixed(0)}',
style: TextStyle(fontWeight: FontWeight.bold, color: typeColor),
),
if (t.investmentStatus == 'OPEN' && (typeColor == NatureColors.getColor('INVESTMENTS') || typeColor == NatureColors.getColor('RECEIVABLES')))
GestureDetector(
onTap: () {
showDialog(context: context, builder: (_) => MaturityDialog(transaction: t));
},
child: const Padding(
padding: EdgeInsets.only(top: 4.0),
child: Text('Close', style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold, fontSize: 12)),
),
),
],
),
),
],
String fromName = '';
if (t.fromWalletId != null && walletsState.hasValue) {
final match = walletsState.value!.where((w) => w.id == t.fromWalletId);
if (match.isNotEmpty) fromName = match.first.name;
}
String toName = '';
if (t.toWalletId != null && walletsState.hasValue) {
final match = walletsState.value!.where((w) => w.id == t.toWalletId);
if (match.isNotEmpty) toName = match.first.name;
}
String subtitleText = '';
if (categoryName == 'Unknown' && fromName.isNotEmpty && toName.isNotEmpty) {
subtitleText = '$fromName$toName${DateFormat('MMM dd, yyyy').format(t.date)}';
} else if (categoryName == 'Unknown' && toName.isNotEmpty) {
subtitleText = 'To $toName${DateFormat('MMM dd, yyyy').format(t.date)}';
} else if (categoryName == 'Unknown' && fromName.isNotEmpty) {
subtitleText = 'From $fromName${DateFormat('MMM dd, yyyy').format(t.date)}';
} else {
subtitleText = '$categoryName${DateFormat('MMM dd, yyyy').format(t.date)}${fromName.isNotEmpty ? ' • 💼 $fromName' : ''}';
}
return Dismissible(
key: Key(t.id.toString()),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
child: const Icon(LucideIcons.trash2, color: Colors.white),
),
),
);
},
),
);
}
confirmDismiss: (dir) async {
return await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Delete Transaction?'),
content: const Text('Are you sure you want to delete this transaction?'),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel')),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('Delete', style: TextStyle(color: Colors.red)),
),
],
),
);
},
onDismissed: (dir) {
ref.read(transactionProvider.notifier).deleteTransaction(t.id);
ref.read(paginatedTransactionProvider.notifier).removeTransactionFromState(t.id);
},
child: Card(
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => AddTransactionScreen(transaction: t)));
},
leading: CircleAvatar(
backgroundColor: typeColor.withValues(alpha: 0.1),
child: Icon(typeIcon, color: typeColor),
),
title: Text((t.description != null && t.description!.isNotEmpty) ? t.description! : accountName, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(subtitleText),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${isIncome ? '+' : (isTransfer ? '' : '-')}Rs. ${t.amount.toStringAsFixed(0)}',
style: TextStyle(fontWeight: FontWeight.bold, color: typeColor),
),
if (t.investmentStatus == 'OPEN' && (typeColor == NatureColors.getColor('INVESTMENTS') || typeColor == NatureColors.getColor('RECEIVABLES')))
GestureDetector(
onTap: () {
showDialog(context: context, builder: (_) => MaturityDialog(transaction: t));
},
child: const Padding(
padding: EdgeInsets.only(top: 4.0),
child: Text('Close', style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold, fontSize: 12)),
),
),
],
),
),
),
);
},
),
);
},
),
),
);
}

View File

@@ -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 '../../providers/providers.dart';
import '../../providers/paginated_transaction_provider.dart';

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../providers/vendors_provider.dart';
import '../domain/vendor.dart';
import 'dart:async';
@@ -209,7 +210,7 @@ class _AddVendorSheetState extends ConsumerState<AddVendorSheet> {
radius: 50,
backgroundColor: Colors.blue.withOpacity(0.1),
backgroundImage: _photo != null
? FileImage(File(_photo!.path)) as ImageProvider
? (kIsWeb ? NetworkImage(_photo!.path) : FileImage(File(_photo!.path))) as ImageProvider
: (widget.vendor?.photoUrl != null
? NetworkImage(widget.vendor!.photoUrl!)
: null),

View File

@@ -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 '../../../core/widgets/premium_text_field.dart';
import '../../../core/widgets/smart_search_dropdown.dart';

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.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 'dart:io';
import 'package:dio/dio.dart';
@@ -897,7 +898,7 @@ class _PurchaseOrderBuilderScreenState
context,
MaterialPageRoute(
builder: (_) => AttachmentGalleryScreen(
images: [FileImage(File(_invoiceFile!.path))],
images: [kIsWeb ? NetworkImage(_invoiceFile!.path) : FileImage(File(_invoiceFile!.path))],
initialIndex: 0,
),
),
@@ -1362,7 +1363,7 @@ class _POItemRowState extends State<_POItemRow> {
context,
MaterialPageRoute(
builder: (_) => AttachmentGalleryScreen(
images: [FileImage(File(widget.item.localPhotoPath!))],
images: [kIsWeb ? NetworkImage(widget.item.localPhotoPath!) : FileImage(File(widget.item.localPhotoPath!))],
initialIndex: 0,
),
),
@@ -1396,7 +1397,9 @@ class _POItemRowState extends State<_POItemRow> {
child: widget.item.localPhotoPath != null
? ClipRRect(
borderRadius: BorderRadius.circular(7),
child: Image.file(File(widget.item.localPhotoPath!), fit: BoxFit.cover),
child: kIsWeb
? Image.network(widget.item.localPhotoPath!, fit: BoxFit.cover)
: Image.file(File(widget.item.localPhotoPath!), fit: BoxFit.cover),
)
: widget.item.photoUrl != null && widget.item.photoUrl!.isNotEmpty
? ClipRRect(

View File

@@ -1,7 +1,7 @@
import 'dart:io';
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 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';

View File

@@ -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/purchase_orders_provider.dart';
import '../providers/vendors_provider.dart';

View File

@@ -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 '../providers/vendors_provider.dart';
import 'add_vendor_sheet.dart';

View File

@@ -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 '../../../../core/widgets/premium_text_field.dart';
import '../../../../core/widgets/smart_search_dropdown.dart';
import '../../providers/vendors_provider.dart';

View File

@@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/network/dio_client.dart';
import '../domain/purchase_order.dart';
import '../domain/purchase_payment.dart';
import '../../inventory/providers/products_provider.dart';
class PurchaseOrdersNotifier extends AsyncNotifier<List<PurchaseOrder>> {
@override
@@ -84,6 +85,7 @@ class PurchaseOrdersNotifier extends AsyncNotifier<List<PurchaseOrder>> {
state = AsyncValue.data(
current.map((c) => c.id == updatedPo.id ? updatedPo : c).toList(),
);
ref.invalidate(productsProvider);
return updatedPo;
}
throw Exception('Failed to receive purchase invoice');