runable
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import '../models/category_model.dart';
|
||||
|
||||
/// Category local data source using Hive
|
||||
abstract class CategoryLocalDataSource {
|
||||
Future<List<CategoryModel>> getAllCategories();
|
||||
Future<CategoryModel?> getCategoryById(String id);
|
||||
Future<void> cacheCategories(List<CategoryModel> categories);
|
||||
Future<void> clearCategories();
|
||||
}
|
||||
|
||||
class CategoryLocalDataSourceImpl implements CategoryLocalDataSource {
|
||||
final Box<CategoryModel> box;
|
||||
|
||||
CategoryLocalDataSourceImpl(this.box);
|
||||
|
||||
@override
|
||||
Future<List<CategoryModel>> getAllCategories() async {
|
||||
return box.values.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CategoryModel?> getCategoryById(String id) async {
|
||||
return box.get(id);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> cacheCategories(List<CategoryModel> categories) async {
|
||||
final categoryMap = {for (var c in categories) c.id: c};
|
||||
await box.putAll(categoryMap);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearCategories() async {
|
||||
await box.clear();
|
||||
}
|
||||
}
|
||||
112
lib/features/categories/data/models/category_model.dart
Normal file
112
lib/features/categories/data/models/category_model.dart
Normal file
@@ -0,0 +1,112 @@
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import '../../domain/entities/category.dart';
|
||||
import '../../../../core/constants/storage_constants.dart';
|
||||
|
||||
part 'category_model.g.dart';
|
||||
|
||||
@HiveType(typeId: StorageConstants.categoryTypeId)
|
||||
class CategoryModel extends HiveObject {
|
||||
@HiveField(0)
|
||||
final String id;
|
||||
|
||||
@HiveField(1)
|
||||
final String name;
|
||||
|
||||
@HiveField(2)
|
||||
final String? description;
|
||||
|
||||
@HiveField(3)
|
||||
final String? iconPath;
|
||||
|
||||
@HiveField(4)
|
||||
final String? color;
|
||||
|
||||
@HiveField(5)
|
||||
final int productCount;
|
||||
|
||||
@HiveField(6)
|
||||
final DateTime createdAt;
|
||||
|
||||
CategoryModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.description,
|
||||
this.iconPath,
|
||||
this.color,
|
||||
required this.productCount,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
/// Convert to domain entity
|
||||
Category toEntity() {
|
||||
return Category(
|
||||
id: id,
|
||||
name: name,
|
||||
description: description,
|
||||
iconPath: iconPath,
|
||||
color: color,
|
||||
productCount: productCount,
|
||||
createdAt: createdAt,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create from domain entity
|
||||
factory CategoryModel.fromEntity(Category category) {
|
||||
return CategoryModel(
|
||||
id: category.id,
|
||||
name: category.name,
|
||||
description: category.description,
|
||||
iconPath: category.iconPath,
|
||||
color: category.color,
|
||||
productCount: category.productCount,
|
||||
createdAt: category.createdAt,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create from JSON
|
||||
factory CategoryModel.fromJson(Map<String, dynamic> json) {
|
||||
return CategoryModel(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String?,
|
||||
iconPath: json['iconPath'] as String?,
|
||||
color: json['color'] as String?,
|
||||
productCount: json['productCount'] as int,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'description': description,
|
||||
'iconPath': iconPath,
|
||||
'color': color,
|
||||
'productCount': productCount,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a copy with updated fields
|
||||
CategoryModel copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
String? description,
|
||||
String? iconPath,
|
||||
String? color,
|
||||
int? productCount,
|
||||
DateTime? createdAt,
|
||||
}) {
|
||||
return CategoryModel(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
description: description ?? this.description,
|
||||
iconPath: iconPath ?? this.iconPath,
|
||||
color: color ?? this.color,
|
||||
productCount: productCount ?? this.productCount,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
59
lib/features/categories/data/models/category_model.g.dart
Normal file
59
lib/features/categories/data/models/category_model.g.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'category_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class CategoryModelAdapter extends TypeAdapter<CategoryModel> {
|
||||
@override
|
||||
final typeId = 1;
|
||||
|
||||
@override
|
||||
CategoryModel read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return CategoryModel(
|
||||
id: fields[0] as String,
|
||||
name: fields[1] as String,
|
||||
description: fields[2] as String?,
|
||||
iconPath: fields[3] as String?,
|
||||
color: fields[4] as String?,
|
||||
productCount: (fields[5] as num).toInt(),
|
||||
createdAt: fields[6] as DateTime,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, CategoryModel obj) {
|
||||
writer
|
||||
..writeByte(7)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
..write(obj.name)
|
||||
..writeByte(2)
|
||||
..write(obj.description)
|
||||
..writeByte(3)
|
||||
..write(obj.iconPath)
|
||||
..writeByte(4)
|
||||
..write(obj.color)
|
||||
..writeByte(5)
|
||||
..write(obj.productCount)
|
||||
..writeByte(6)
|
||||
..write(obj.createdAt);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is CategoryModelAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../domain/entities/category.dart';
|
||||
import '../../domain/repositories/category_repository.dart';
|
||||
import '../datasources/category_local_datasource.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../../../../core/errors/exceptions.dart';
|
||||
|
||||
class CategoryRepositoryImpl implements CategoryRepository {
|
||||
final CategoryLocalDataSource localDataSource;
|
||||
|
||||
CategoryRepositoryImpl({
|
||||
required this.localDataSource,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<Category>>> getAllCategories() async {
|
||||
try {
|
||||
final categories = await localDataSource.getAllCategories();
|
||||
return Right(categories.map((model) => model.toEntity()).toList());
|
||||
} on CacheException catch (e) {
|
||||
return Left(CacheFailure(e.message));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Category>> getCategoryById(String id) async {
|
||||
try {
|
||||
final category = await localDataSource.getCategoryById(id);
|
||||
if (category == null) {
|
||||
return Left(NotFoundFailure('Category not found'));
|
||||
}
|
||||
return Right(category.toEntity());
|
||||
} on CacheException catch (e) {
|
||||
return Left(CacheFailure(e.message));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<Category>>> syncCategories() async {
|
||||
try {
|
||||
// For now, return cached categories
|
||||
// In the future, implement remote sync
|
||||
final categories = await localDataSource.getAllCategories();
|
||||
return Right(categories.map((model) => model.toEntity()).toList());
|
||||
} on CacheException catch (e) {
|
||||
return Left(CacheFailure(e.message));
|
||||
}
|
||||
}
|
||||
}
|
||||
33
lib/features/categories/domain/entities/category.dart
Normal file
33
lib/features/categories/domain/entities/category.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Category domain entity
|
||||
class Category extends Equatable {
|
||||
final String id;
|
||||
final String name;
|
||||
final String? description;
|
||||
final String? iconPath;
|
||||
final String? color;
|
||||
final int productCount;
|
||||
final DateTime createdAt;
|
||||
|
||||
const Category({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.description,
|
||||
this.iconPath,
|
||||
this.color,
|
||||
required this.productCount,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
iconPath,
|
||||
color,
|
||||
productCount,
|
||||
createdAt,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../entities/category.dart';
|
||||
|
||||
/// Category repository interface
|
||||
abstract class CategoryRepository {
|
||||
/// Get all categories from cache
|
||||
Future<Either<Failure, List<Category>>> getAllCategories();
|
||||
|
||||
/// Get category by ID
|
||||
Future<Either<Failure, Category>> getCategoryById(String id);
|
||||
|
||||
/// Sync categories from remote
|
||||
Future<Either<Failure, List<Category>>> syncCategories();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../entities/category.dart';
|
||||
import '../repositories/category_repository.dart';
|
||||
|
||||
/// Use case to get all categories
|
||||
class GetAllCategories {
|
||||
final CategoryRepository repository;
|
||||
|
||||
GetAllCategories(this.repository);
|
||||
|
||||
Future<Either<Failure, List<Category>>> call() async {
|
||||
return await repository.getAllCategories();
|
||||
}
|
||||
}
|
||||
116
lib/features/categories/presentation/pages/categories_page.dart
Normal file
116
lib/features/categories/presentation/pages/categories_page.dart
Normal file
@@ -0,0 +1,116 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../widgets/category_grid.dart';
|
||||
import '../providers/categories_provider.dart';
|
||||
import '../../../products/presentation/providers/selected_category_provider.dart' as product_providers;
|
||||
|
||||
/// Categories page - displays all categories in a grid
|
||||
class CategoriesPage extends ConsumerWidget {
|
||||
const CategoriesPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final categoriesAsync = ref.watch(categoriesProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Categories'),
|
||||
actions: [
|
||||
// Refresh button
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
tooltip: 'Refresh categories',
|
||||
onPressed: () {
|
||||
ref.invalidate(categoriesProvider);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await ref.refresh(categoriesProvider.future);
|
||||
},
|
||||
child: categoriesAsync.when(
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
error: (error, stack) => Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Error loading categories',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
error.toString(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => ref.invalidate(categoriesProvider),
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Retry'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
data: (categories) {
|
||||
return Column(
|
||||
children: [
|
||||
// Categories count
|
||||
if (categories.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'${categories.length} categor${categories.length == 1 ? 'y' : 'ies'}',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Category grid
|
||||
Expanded(
|
||||
child: CategoryGrid(
|
||||
onCategoryTap: (category) {
|
||||
// Set selected category
|
||||
ref
|
||||
.read(product_providers.selectedCategoryProvider.notifier)
|
||||
.selectCategory(category.id);
|
||||
|
||||
// Show snackbar
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Filtering products by ${category.name}',
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
action: SnackBarAction(
|
||||
label: 'View',
|
||||
onPressed: () {
|
||||
// Navigate to products tab
|
||||
// This will be handled by the parent widget
|
||||
// For now, just show a message
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import '../../domain/entities/category.dart';
|
||||
|
||||
part 'categories_provider.g.dart';
|
||||
|
||||
/// Provider for categories list
|
||||
@riverpod
|
||||
class Categories extends _$Categories {
|
||||
@override
|
||||
Future<List<Category>> build() async {
|
||||
// TODO: Implement with repository
|
||||
return [];
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
// Fetch categories from repository
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> syncCategories() async {
|
||||
// TODO: Implement sync logic with remote data source
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
// Sync categories from API
|
||||
return [];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider for selected category
|
||||
@riverpod
|
||||
class SelectedCategory extends _$SelectedCategory {
|
||||
@override
|
||||
String? build() => null;
|
||||
|
||||
void select(String? categoryId) {
|
||||
state = categoryId;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
state = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'categories_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Provider for categories list
|
||||
|
||||
@ProviderFor(Categories)
|
||||
const categoriesProvider = CategoriesProvider._();
|
||||
|
||||
/// Provider for categories list
|
||||
final class CategoriesProvider
|
||||
extends $AsyncNotifierProvider<Categories, List<Category>> {
|
||||
/// Provider for categories list
|
||||
const CategoriesProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'categoriesProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$categoriesHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
Categories create() => Categories();
|
||||
}
|
||||
|
||||
String _$categoriesHash() => r'aa7afc38a5567b0f42ff05ca23b287baa4780cbe';
|
||||
|
||||
/// Provider for categories list
|
||||
|
||||
abstract class _$Categories extends $AsyncNotifier<List<Category>> {
|
||||
FutureOr<List<Category>> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<AsyncValue<List<Category>>, List<Category>>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<List<Category>>, List<Category>>,
|
||||
AsyncValue<List<Category>>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider for selected category
|
||||
|
||||
@ProviderFor(SelectedCategory)
|
||||
const selectedCategoryProvider = SelectedCategoryProvider._();
|
||||
|
||||
/// Provider for selected category
|
||||
final class SelectedCategoryProvider
|
||||
extends $NotifierProvider<SelectedCategory, String?> {
|
||||
/// Provider for selected category
|
||||
const SelectedCategoryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'selectedCategoryProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$selectedCategoryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
SelectedCategory create() => SelectedCategory();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(String? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<String?>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$selectedCategoryHash() => r'a47cd2de07ad285d4b73b2294ba954cb1cdd8e4c';
|
||||
|
||||
/// Provider for selected category
|
||||
|
||||
abstract class _$SelectedCategory extends $Notifier<String?> {
|
||||
String? build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<String?, String?>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<String?, String?>,
|
||||
String?,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import '../../data/datasources/category_local_datasource.dart';
|
||||
import '../../../../core/database/hive_database.dart';
|
||||
import '../../data/models/category_model.dart';
|
||||
|
||||
part 'category_datasource_provider.g.dart';
|
||||
|
||||
/// Provider for category local data source
|
||||
/// This is kept alive as it's a dependency injection provider
|
||||
@Riverpod(keepAlive: true)
|
||||
CategoryLocalDataSource categoryLocalDataSource(Ref ref) {
|
||||
final box = HiveDatabase.instance.getBox<CategoryModel>('categories');
|
||||
return CategoryLocalDataSourceImpl(box);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'category_datasource_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Provider for category local data source
|
||||
/// This is kept alive as it's a dependency injection provider
|
||||
|
||||
@ProviderFor(categoryLocalDataSource)
|
||||
const categoryLocalDataSourceProvider = CategoryLocalDataSourceProvider._();
|
||||
|
||||
/// Provider for category local data source
|
||||
/// This is kept alive as it's a dependency injection provider
|
||||
|
||||
final class CategoryLocalDataSourceProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
CategoryLocalDataSource,
|
||||
CategoryLocalDataSource,
|
||||
CategoryLocalDataSource
|
||||
>
|
||||
with $Provider<CategoryLocalDataSource> {
|
||||
/// Provider for category local data source
|
||||
/// This is kept alive as it's a dependency injection provider
|
||||
const CategoryLocalDataSourceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'categoryLocalDataSourceProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$categoryLocalDataSourceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<CategoryLocalDataSource> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
CategoryLocalDataSource create(Ref ref) {
|
||||
return categoryLocalDataSource(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(CategoryLocalDataSource value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<CategoryLocalDataSource>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$categoryLocalDataSourceHash() =>
|
||||
r'1f8412f2dc76a348873f1da4f76ae4a08991f269';
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import '../../../products/presentation/providers/products_provider.dart';
|
||||
|
||||
part 'category_product_count_provider.g.dart';
|
||||
|
||||
/// Provider that calculates product count for a specific category
|
||||
/// Uses family pattern to create a provider for each category ID
|
||||
@riverpod
|
||||
int categoryProductCount(Ref ref, String categoryId) {
|
||||
final productsAsync = ref.watch(productsProvider);
|
||||
return productsAsync.when(
|
||||
data: (products) => products.where((p) => p.categoryId == categoryId).length,
|
||||
loading: () => 0,
|
||||
error: (_, __) => 0,
|
||||
);
|
||||
}
|
||||
|
||||
/// Provider that returns all category product counts as a map
|
||||
/// Useful for displaying product counts on all category cards at once
|
||||
@riverpod
|
||||
Map<String, int> allCategoryProductCounts(Ref ref) {
|
||||
final productsAsync = ref.watch(productsProvider);
|
||||
return productsAsync.when(
|
||||
data: (products) {
|
||||
// Group products by category and count
|
||||
final counts = <String, int>{};
|
||||
for (final product in products) {
|
||||
counts[product.categoryId] = (counts[product.categoryId] ?? 0) + 1;
|
||||
}
|
||||
return counts;
|
||||
},
|
||||
loading: () => {},
|
||||
error: (_, __) => {},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'category_product_count_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Provider that calculates product count for a specific category
|
||||
/// Uses family pattern to create a provider for each category ID
|
||||
|
||||
@ProviderFor(categoryProductCount)
|
||||
const categoryProductCountProvider = CategoryProductCountFamily._();
|
||||
|
||||
/// Provider that calculates product count for a specific category
|
||||
/// Uses family pattern to create a provider for each category ID
|
||||
|
||||
final class CategoryProductCountProvider
|
||||
extends $FunctionalProvider<int, int, int>
|
||||
with $Provider<int> {
|
||||
/// Provider that calculates product count for a specific category
|
||||
/// Uses family pattern to create a provider for each category ID
|
||||
const CategoryProductCountProvider._({
|
||||
required CategoryProductCountFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'categoryProductCountProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$categoryProductCountHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'categoryProductCountProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<int> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
int create(Ref ref) {
|
||||
final argument = this.argument as String;
|
||||
return categoryProductCount(ref, argument);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(int value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<int>(value),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is CategoryProductCountProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$categoryProductCountHash() =>
|
||||
r'2d51eea21a4d018964d10ee00d0957a2c38d28c6';
|
||||
|
||||
/// Provider that calculates product count for a specific category
|
||||
/// Uses family pattern to create a provider for each category ID
|
||||
|
||||
final class CategoryProductCountFamily extends $Family
|
||||
with $FunctionalFamilyOverride<int, String> {
|
||||
const CategoryProductCountFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'categoryProductCountProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
/// Provider that calculates product count for a specific category
|
||||
/// Uses family pattern to create a provider for each category ID
|
||||
|
||||
CategoryProductCountProvider call(String categoryId) =>
|
||||
CategoryProductCountProvider._(argument: categoryId, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'categoryProductCountProvider';
|
||||
}
|
||||
|
||||
/// Provider that returns all category product counts as a map
|
||||
/// Useful for displaying product counts on all category cards at once
|
||||
|
||||
@ProviderFor(allCategoryProductCounts)
|
||||
const allCategoryProductCountsProvider = AllCategoryProductCountsProvider._();
|
||||
|
||||
/// Provider that returns all category product counts as a map
|
||||
/// Useful for displaying product counts on all category cards at once
|
||||
|
||||
final class AllCategoryProductCountsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
Map<String, int>,
|
||||
Map<String, int>,
|
||||
Map<String, int>
|
||||
>
|
||||
with $Provider<Map<String, int>> {
|
||||
/// Provider that returns all category product counts as a map
|
||||
/// Useful for displaying product counts on all category cards at once
|
||||
const AllCategoryProductCountsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'allCategoryProductCountsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$allCategoryProductCountsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<Map<String, int>> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Map<String, int> create(Ref ref) {
|
||||
return allCategoryProductCounts(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(Map<String, int> value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<Map<String, int>>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$allCategoryProductCountsHash() =>
|
||||
r'a4ecc281916772ac74327333bd76e7b6463a0992';
|
||||
@@ -0,0 +1,4 @@
|
||||
/// Export all category providers
|
||||
export 'category_datasource_provider.dart';
|
||||
export 'categories_provider.dart';
|
||||
export 'category_product_count_provider.dart';
|
||||
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../domain/entities/category.dart';
|
||||
|
||||
/// Category card widget
|
||||
class CategoryCard extends StatelessWidget {
|
||||
final Category category;
|
||||
|
||||
const CategoryCard({
|
||||
super.key,
|
||||
required this.category,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = category.color != null
|
||||
? Color(int.parse(category.color!.substring(1), radix: 16) + 0xFF000000)
|
||||
: Theme.of(context).colorScheme.primaryContainer;
|
||||
|
||||
return Card(
|
||||
color: color,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
// TODO: Filter products by category
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.category,
|
||||
size: 48,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
category.name,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${category.productCount} products',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../providers/categories_provider.dart';
|
||||
import '../../domain/entities/category.dart';
|
||||
import 'category_card.dart';
|
||||
import '../../../../core/widgets/loading_indicator.dart';
|
||||
import '../../../../core/widgets/error_widget.dart';
|
||||
import '../../../../core/widgets/empty_state.dart';
|
||||
|
||||
/// Category grid widget
|
||||
class CategoryGrid extends ConsumerWidget {
|
||||
final void Function(Category)? onCategoryTap;
|
||||
|
||||
const CategoryGrid({
|
||||
super.key,
|
||||
this.onCategoryTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final categoriesAsync = ref.watch(categoriesProvider);
|
||||
|
||||
return categoriesAsync.when(
|
||||
loading: () => const LoadingIndicator(message: 'Loading categories...'),
|
||||
error: (error, stack) => ErrorDisplay(
|
||||
message: error.toString(),
|
||||
onRetry: () => ref.refresh(categoriesProvider),
|
||||
),
|
||||
data: (categories) {
|
||||
if (categories.isEmpty) {
|
||||
return const EmptyState(
|
||||
message: 'No categories found',
|
||||
subMessage: 'Categories will appear here once added',
|
||||
icon: Icons.category_outlined,
|
||||
);
|
||||
}
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// Determine grid columns based on width
|
||||
int crossAxisCount = 2;
|
||||
if (constraints.maxWidth > 1200) {
|
||||
crossAxisCount = 4;
|
||||
} else if (constraints.maxWidth > 800) {
|
||||
crossAxisCount = 3;
|
||||
}
|
||||
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
childAspectRatio: 1.2,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
),
|
||||
itemCount: categories.length,
|
||||
itemBuilder: (context, index) {
|
||||
final category = categories[index];
|
||||
return RepaintBoundary(
|
||||
child: GestureDetector(
|
||||
onTap: () => onCategoryTap?.call(category),
|
||||
child: CategoryCard(category: category),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Category Feature Widgets
|
||||
export 'category_card.dart';
|
||||
export 'category_grid.dart';
|
||||
|
||||
// This file provides a central export point for all category widgets
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import '../models/cart_item_model.dart';
|
||||
|
||||
/// Cart local data source using Hive
|
||||
abstract class CartLocalDataSource {
|
||||
Future<List<CartItemModel>> getCartItems();
|
||||
Future<void> addToCart(CartItemModel item);
|
||||
Future<void> updateQuantity(String productId, int quantity);
|
||||
Future<void> removeFromCart(String productId);
|
||||
Future<void> clearCart();
|
||||
}
|
||||
|
||||
class CartLocalDataSourceImpl implements CartLocalDataSource {
|
||||
final Box<CartItemModel> box;
|
||||
|
||||
CartLocalDataSourceImpl(this.box);
|
||||
|
||||
@override
|
||||
Future<List<CartItemModel>> getCartItems() async {
|
||||
return box.values.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> addToCart(CartItemModel item) async {
|
||||
await box.put(item.productId, item);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateQuantity(String productId, int quantity) async {
|
||||
final item = box.get(productId);
|
||||
if (item != null) {
|
||||
final updated = CartItemModel(
|
||||
productId: item.productId,
|
||||
productName: item.productName,
|
||||
price: item.price,
|
||||
quantity: quantity,
|
||||
imageUrl: item.imageUrl,
|
||||
addedAt: item.addedAt,
|
||||
);
|
||||
await box.put(productId, updated);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> removeFromCart(String productId) async {
|
||||
await box.delete(productId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearCart() async {
|
||||
await box.clear();
|
||||
}
|
||||
}
|
||||
83
lib/features/home/data/models/cart_item_model.dart
Normal file
83
lib/features/home/data/models/cart_item_model.dart
Normal file
@@ -0,0 +1,83 @@
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import '../../domain/entities/cart_item.dart';
|
||||
import '../../../../core/constants/storage_constants.dart';
|
||||
|
||||
part 'cart_item_model.g.dart';
|
||||
|
||||
@HiveType(typeId: StorageConstants.cartItemTypeId)
|
||||
class CartItemModel extends HiveObject {
|
||||
@HiveField(0)
|
||||
final String productId;
|
||||
|
||||
@HiveField(1)
|
||||
final String productName;
|
||||
|
||||
@HiveField(2)
|
||||
final double price;
|
||||
|
||||
@HiveField(3)
|
||||
final int quantity;
|
||||
|
||||
@HiveField(4)
|
||||
final String? imageUrl;
|
||||
|
||||
@HiveField(5)
|
||||
final DateTime addedAt;
|
||||
|
||||
CartItemModel({
|
||||
required this.productId,
|
||||
required this.productName,
|
||||
required this.price,
|
||||
required this.quantity,
|
||||
this.imageUrl,
|
||||
required this.addedAt,
|
||||
});
|
||||
|
||||
/// Convert to domain entity
|
||||
CartItem toEntity() {
|
||||
return CartItem(
|
||||
productId: productId,
|
||||
productName: productName,
|
||||
price: price,
|
||||
quantity: quantity,
|
||||
imageUrl: imageUrl,
|
||||
addedAt: addedAt,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create from domain entity
|
||||
factory CartItemModel.fromEntity(CartItem item) {
|
||||
return CartItemModel(
|
||||
productId: item.productId,
|
||||
productName: item.productName,
|
||||
price: item.price,
|
||||
quantity: item.quantity,
|
||||
imageUrl: item.imageUrl,
|
||||
addedAt: item.addedAt,
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'productId': productId,
|
||||
'productName': productName,
|
||||
'price': price,
|
||||
'quantity': quantity,
|
||||
'imageUrl': imageUrl,
|
||||
'addedAt': addedAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Create from JSON
|
||||
factory CartItemModel.fromJson(Map<String, dynamic> json) {
|
||||
return CartItemModel(
|
||||
productId: json['productId'] as String,
|
||||
productName: json['productName'] as String,
|
||||
price: (json['price'] as num).toDouble(),
|
||||
quantity: json['quantity'] as int,
|
||||
imageUrl: json['imageUrl'] as String?,
|
||||
addedAt: DateTime.parse(json['addedAt'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
56
lib/features/home/data/models/cart_item_model.g.dart
Normal file
56
lib/features/home/data/models/cart_item_model.g.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'cart_item_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class CartItemModelAdapter extends TypeAdapter<CartItemModel> {
|
||||
@override
|
||||
final typeId = 2;
|
||||
|
||||
@override
|
||||
CartItemModel read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return CartItemModel(
|
||||
productId: fields[0] as String,
|
||||
productName: fields[1] as String,
|
||||
price: (fields[2] as num).toDouble(),
|
||||
quantity: (fields[3] as num).toInt(),
|
||||
imageUrl: fields[4] as String?,
|
||||
addedAt: fields[5] as DateTime,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, CartItemModel obj) {
|
||||
writer
|
||||
..writeByte(6)
|
||||
..writeByte(0)
|
||||
..write(obj.productId)
|
||||
..writeByte(1)
|
||||
..write(obj.productName)
|
||||
..writeByte(2)
|
||||
..write(obj.price)
|
||||
..writeByte(3)
|
||||
..write(obj.quantity)
|
||||
..writeByte(4)
|
||||
..write(obj.imageUrl)
|
||||
..writeByte(5)
|
||||
..write(obj.addedAt);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is CartItemModelAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
123
lib/features/home/data/models/transaction_model.dart
Normal file
123
lib/features/home/data/models/transaction_model.dart
Normal file
@@ -0,0 +1,123 @@
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import 'package:retail/core/constants/storage_constants.dart';
|
||||
import 'package:retail/features/home/data/models/cart_item_model.dart';
|
||||
|
||||
part 'transaction_model.g.dart';
|
||||
|
||||
/// Transaction model with Hive CE type adapter
|
||||
@HiveType(typeId: StorageConstants.transactionTypeId)
|
||||
class TransactionModel extends HiveObject {
|
||||
/// Unique transaction identifier
|
||||
@HiveField(0)
|
||||
final String id;
|
||||
|
||||
/// List of cart items in this transaction
|
||||
@HiveField(1)
|
||||
final List<CartItemModel> items;
|
||||
|
||||
/// Subtotal amount (before tax and discount)
|
||||
@HiveField(2)
|
||||
final double subtotal;
|
||||
|
||||
/// Tax amount
|
||||
@HiveField(3)
|
||||
final double tax;
|
||||
|
||||
/// Discount amount
|
||||
@HiveField(4)
|
||||
final double discount;
|
||||
|
||||
/// Total amount (subtotal + tax - discount)
|
||||
@HiveField(5)
|
||||
final double total;
|
||||
|
||||
/// Transaction completion timestamp
|
||||
@HiveField(6)
|
||||
final DateTime completedAt;
|
||||
|
||||
/// Payment method used (e.g., 'cash', 'card', 'digital')
|
||||
@HiveField(7)
|
||||
final String paymentMethod;
|
||||
|
||||
TransactionModel({
|
||||
required this.id,
|
||||
required this.items,
|
||||
required this.subtotal,
|
||||
required this.tax,
|
||||
required this.discount,
|
||||
required this.total,
|
||||
required this.completedAt,
|
||||
required this.paymentMethod,
|
||||
});
|
||||
|
||||
/// Get total number of items in transaction
|
||||
int get totalItems => items.fold(0, (sum, item) => sum + item.quantity);
|
||||
|
||||
/// Create a copy with updated fields
|
||||
TransactionModel copyWith({
|
||||
String? id,
|
||||
List<CartItemModel>? items,
|
||||
double? subtotal,
|
||||
double? tax,
|
||||
double? discount,
|
||||
double? total,
|
||||
DateTime? completedAt,
|
||||
String? paymentMethod,
|
||||
}) {
|
||||
return TransactionModel(
|
||||
id: id ?? this.id,
|
||||
items: items ?? this.items,
|
||||
subtotal: subtotal ?? this.subtotal,
|
||||
tax: tax ?? this.tax,
|
||||
discount: discount ?? this.discount,
|
||||
total: total ?? this.total,
|
||||
completedAt: completedAt ?? this.completedAt,
|
||||
paymentMethod: paymentMethod ?? this.paymentMethod,
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'items': items.map((item) => item.toJson()).toList(),
|
||||
'subtotal': subtotal,
|
||||
'tax': tax,
|
||||
'discount': discount,
|
||||
'total': total,
|
||||
'completedAt': completedAt.toIso8601String(),
|
||||
'paymentMethod': paymentMethod,
|
||||
};
|
||||
}
|
||||
|
||||
/// Create from JSON
|
||||
factory TransactionModel.fromJson(Map<String, dynamic> json) {
|
||||
return TransactionModel(
|
||||
id: json['id'] as String,
|
||||
items: (json['items'] as List)
|
||||
.map((item) => CartItemModel.fromJson(item as Map<String, dynamic>))
|
||||
.toList(),
|
||||
subtotal: (json['subtotal'] as num).toDouble(),
|
||||
tax: (json['tax'] as num).toDouble(),
|
||||
discount: (json['discount'] as num).toDouble(),
|
||||
total: (json['total'] as num).toDouble(),
|
||||
completedAt: DateTime.parse(json['completedAt'] as String),
|
||||
paymentMethod: json['paymentMethod'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TransactionModel(id: $id, total: $total, items: ${items.length}, method: $paymentMethod)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is TransactionModel && other.id == id;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
}
|
||||
62
lib/features/home/data/models/transaction_model.g.dart
Normal file
62
lib/features/home/data/models/transaction_model.g.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'transaction_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class TransactionModelAdapter extends TypeAdapter<TransactionModel> {
|
||||
@override
|
||||
final typeId = 3;
|
||||
|
||||
@override
|
||||
TransactionModel read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return TransactionModel(
|
||||
id: fields[0] as String,
|
||||
items: (fields[1] as List).cast<CartItemModel>(),
|
||||
subtotal: (fields[2] as num).toDouble(),
|
||||
tax: (fields[3] as num).toDouble(),
|
||||
discount: (fields[4] as num).toDouble(),
|
||||
total: (fields[5] as num).toDouble(),
|
||||
completedAt: fields[6] as DateTime,
|
||||
paymentMethod: fields[7] as String,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, TransactionModel obj) {
|
||||
writer
|
||||
..writeByte(8)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
..write(obj.items)
|
||||
..writeByte(2)
|
||||
..write(obj.subtotal)
|
||||
..writeByte(3)
|
||||
..write(obj.tax)
|
||||
..writeByte(4)
|
||||
..write(obj.discount)
|
||||
..writeByte(5)
|
||||
..write(obj.total)
|
||||
..writeByte(6)
|
||||
..write(obj.completedAt)
|
||||
..writeByte(7)
|
||||
..write(obj.paymentMethod);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is TransactionModelAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../domain/entities/cart_item.dart';
|
||||
import '../../domain/repositories/cart_repository.dart';
|
||||
import '../datasources/cart_local_datasource.dart';
|
||||
import '../models/cart_item_model.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../../../../core/errors/exceptions.dart';
|
||||
|
||||
class CartRepositoryImpl implements CartRepository {
|
||||
final CartLocalDataSource localDataSource;
|
||||
|
||||
CartRepositoryImpl({
|
||||
required this.localDataSource,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<CartItem>>> getCartItems() async {
|
||||
try {
|
||||
final items = await localDataSource.getCartItems();
|
||||
return Right(items.map((model) => model.toEntity()).toList());
|
||||
} on CacheException catch (e) {
|
||||
return Left(CacheFailure(e.message));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> addToCart(CartItem item) async {
|
||||
try {
|
||||
final model = CartItemModel.fromEntity(item);
|
||||
await localDataSource.addToCart(model);
|
||||
return const Right(null);
|
||||
} on CacheException catch (e) {
|
||||
return Left(CacheFailure(e.message));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> updateQuantity(String productId, int quantity) async {
|
||||
try {
|
||||
await localDataSource.updateQuantity(productId, quantity);
|
||||
return const Right(null);
|
||||
} on CacheException catch (e) {
|
||||
return Left(CacheFailure(e.message));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> removeFromCart(String productId) async {
|
||||
try {
|
||||
await localDataSource.removeFromCart(productId);
|
||||
return const Right(null);
|
||||
} on CacheException catch (e) {
|
||||
return Left(CacheFailure(e.message));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> clearCart() async {
|
||||
try {
|
||||
await localDataSource.clearCart();
|
||||
return const Right(null);
|
||||
} on CacheException catch (e) {
|
||||
return Left(CacheFailure(e.message));
|
||||
}
|
||||
}
|
||||
}
|
||||
50
lib/features/home/domain/entities/cart_item.dart
Normal file
50
lib/features/home/domain/entities/cart_item.dart
Normal file
@@ -0,0 +1,50 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Cart item domain entity
|
||||
class CartItem extends Equatable {
|
||||
final String productId;
|
||||
final String productName;
|
||||
final double price;
|
||||
final int quantity;
|
||||
final String? imageUrl;
|
||||
final DateTime addedAt;
|
||||
|
||||
const CartItem({
|
||||
required this.productId,
|
||||
required this.productName,
|
||||
required this.price,
|
||||
required this.quantity,
|
||||
this.imageUrl,
|
||||
required this.addedAt,
|
||||
});
|
||||
|
||||
double get total => price * quantity;
|
||||
|
||||
CartItem copyWith({
|
||||
String? productId,
|
||||
String? productName,
|
||||
double? price,
|
||||
int? quantity,
|
||||
String? imageUrl,
|
||||
DateTime? addedAt,
|
||||
}) {
|
||||
return CartItem(
|
||||
productId: productId ?? this.productId,
|
||||
productName: productName ?? this.productName,
|
||||
price: price ?? this.price,
|
||||
quantity: quantity ?? this.quantity,
|
||||
imageUrl: imageUrl ?? this.imageUrl,
|
||||
addedAt: addedAt ?? this.addedAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
productId,
|
||||
productName,
|
||||
price,
|
||||
quantity,
|
||||
imageUrl,
|
||||
addedAt,
|
||||
];
|
||||
}
|
||||
21
lib/features/home/domain/repositories/cart_repository.dart
Normal file
21
lib/features/home/domain/repositories/cart_repository.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../entities/cart_item.dart';
|
||||
|
||||
/// Cart repository interface
|
||||
abstract class CartRepository {
|
||||
/// Get all cart items
|
||||
Future<Either<Failure, List<CartItem>>> getCartItems();
|
||||
|
||||
/// Add item to cart
|
||||
Future<Either<Failure, void>> addToCart(CartItem item);
|
||||
|
||||
/// Update cart item quantity
|
||||
Future<Either<Failure, void>> updateQuantity(String productId, int quantity);
|
||||
|
||||
/// Remove item from cart
|
||||
Future<Either<Failure, void>> removeFromCart(String productId);
|
||||
|
||||
/// Clear all cart items
|
||||
Future<Either<Failure, void>> clearCart();
|
||||
}
|
||||
15
lib/features/home/domain/usecases/add_to_cart.dart
Normal file
15
lib/features/home/domain/usecases/add_to_cart.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../entities/cart_item.dart';
|
||||
import '../repositories/cart_repository.dart';
|
||||
|
||||
/// Use case to add item to cart
|
||||
class AddToCart {
|
||||
final CartRepository repository;
|
||||
|
||||
AddToCart(this.repository);
|
||||
|
||||
Future<Either<Failure, void>> call(CartItem item) async {
|
||||
return await repository.addToCart(item);
|
||||
}
|
||||
}
|
||||
8
lib/features/home/domain/usecases/calculate_total.dart
Normal file
8
lib/features/home/domain/usecases/calculate_total.dart
Normal file
@@ -0,0 +1,8 @@
|
||||
import '../entities/cart_item.dart';
|
||||
|
||||
/// Use case to calculate cart total
|
||||
class CalculateTotal {
|
||||
double call(List<CartItem> items) {
|
||||
return items.fold(0.0, (sum, item) => sum + item.total);
|
||||
}
|
||||
}
|
||||
14
lib/features/home/domain/usecases/clear_cart.dart
Normal file
14
lib/features/home/domain/usecases/clear_cart.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../repositories/cart_repository.dart';
|
||||
|
||||
/// Use case to clear cart
|
||||
class ClearCart {
|
||||
final CartRepository repository;
|
||||
|
||||
ClearCart(this.repository);
|
||||
|
||||
Future<Either<Failure, void>> call() async {
|
||||
return await repository.clearCart();
|
||||
}
|
||||
}
|
||||
14
lib/features/home/domain/usecases/remove_from_cart.dart
Normal file
14
lib/features/home/domain/usecases/remove_from_cart.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../repositories/cart_repository.dart';
|
||||
|
||||
/// Use case to remove item from cart
|
||||
class RemoveFromCart {
|
||||
final CartRepository repository;
|
||||
|
||||
RemoveFromCart(this.repository);
|
||||
|
||||
Future<Either<Failure, void>> call(String productId) async {
|
||||
return await repository.removeFromCart(productId);
|
||||
}
|
||||
}
|
||||
173
lib/features/home/presentation/pages/home_page.dart
Normal file
173
lib/features/home/presentation/pages/home_page.dart
Normal file
@@ -0,0 +1,173 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../widgets/product_selector.dart';
|
||||
import '../widgets/cart_summary.dart';
|
||||
import '../providers/cart_provider.dart';
|
||||
import '../../domain/entities/cart_item.dart';
|
||||
|
||||
/// Home page - POS interface with product selector and cart
|
||||
class HomePage extends ConsumerWidget {
|
||||
const HomePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final cartAsync = ref.watch(cartProvider);
|
||||
final isWideScreen = MediaQuery.of(context).size.width > 600;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Point of Sale'),
|
||||
actions: [
|
||||
// Cart item count badge
|
||||
cartAsync.whenOrNull(
|
||||
data: (items) => items.isNotEmpty
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(right: 16.0),
|
||||
child: Center(
|
||||
child: Badge(
|
||||
label: Text('${items.length}'),
|
||||
child: const Icon(Icons.shopping_cart),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
) ?? const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
body: isWideScreen
|
||||
? Row(
|
||||
children: [
|
||||
// Product selector on left
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: ProductSelector(
|
||||
onProductTap: (product) {
|
||||
_showAddToCartDialog(context, ref, product);
|
||||
},
|
||||
),
|
||||
),
|
||||
// Divider
|
||||
const VerticalDivider(width: 1),
|
||||
// Cart on right
|
||||
const Expanded(
|
||||
flex: 2,
|
||||
child: CartSummary(),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
// Product selector on top
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: ProductSelector(
|
||||
onProductTap: (product) {
|
||||
_showAddToCartDialog(context, ref, product);
|
||||
},
|
||||
),
|
||||
),
|
||||
// Divider
|
||||
const Divider(height: 1),
|
||||
// Cart on bottom
|
||||
const Expanded(
|
||||
flex: 3,
|
||||
child: CartSummary(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddToCartDialog(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
dynamic product,
|
||||
) {
|
||||
int quantity = 1;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setState) => AlertDialog(
|
||||
title: const Text('Add to Cart'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
product.name,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove_circle_outline),
|
||||
onPressed: quantity > 1
|
||||
? () => setState(() => quantity--)
|
||||
: null,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Text(
|
||||
'$quantity',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add_circle_outline),
|
||||
onPressed: quantity < product.stockQuantity
|
||||
? () => setState(() => quantity++)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (product.stockQuantity < 5)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
'Only ${product.stockQuantity} in stock',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton.icon(
|
||||
onPressed: () {
|
||||
// Create cart item from product
|
||||
final cartItem = CartItem(
|
||||
productId: product.id,
|
||||
productName: product.name,
|
||||
price: product.price,
|
||||
quantity: quantity,
|
||||
imageUrl: product.imageUrl,
|
||||
addedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
// Add to cart
|
||||
ref.read(cartProvider.notifier).addItem(cartItem);
|
||||
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Added ${product.name} to cart'),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.add_shopping_cart),
|
||||
label: const Text('Add'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'cart_provider.dart';
|
||||
|
||||
part 'cart_item_count_provider.g.dart';
|
||||
|
||||
/// Provider that calculates total number of items in cart
|
||||
/// This is optimized to only rebuild when the count changes
|
||||
@riverpod
|
||||
int cartItemCount(Ref ref) {
|
||||
final itemsAsync = ref.watch(cartProvider);
|
||||
return itemsAsync.when(
|
||||
data: (items) => items.fold<int>(0, (sum, item) => sum + item.quantity),
|
||||
loading: () => 0,
|
||||
error: (_, __) => 0,
|
||||
);
|
||||
}
|
||||
|
||||
/// Provider that calculates unique items count in cart
|
||||
@riverpod
|
||||
int cartUniqueItemCount(Ref ref) {
|
||||
final itemsAsync = ref.watch(cartProvider);
|
||||
return itemsAsync.when(
|
||||
data: (items) => items.length,
|
||||
loading: () => 0,
|
||||
error: (_, __) => 0,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'cart_item_count_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Provider that calculates total number of items in cart
|
||||
/// This is optimized to only rebuild when the count changes
|
||||
|
||||
@ProviderFor(cartItemCount)
|
||||
const cartItemCountProvider = CartItemCountProvider._();
|
||||
|
||||
/// Provider that calculates total number of items in cart
|
||||
/// This is optimized to only rebuild when the count changes
|
||||
|
||||
final class CartItemCountProvider extends $FunctionalProvider<int, int, int>
|
||||
with $Provider<int> {
|
||||
/// Provider that calculates total number of items in cart
|
||||
/// This is optimized to only rebuild when the count changes
|
||||
const CartItemCountProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'cartItemCountProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$cartItemCountHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<int> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
int create(Ref ref) {
|
||||
return cartItemCount(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(int value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<int>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$cartItemCountHash() => r'78fe81648a02fb84477df3be3f08b27caa039203';
|
||||
|
||||
/// Provider that calculates unique items count in cart
|
||||
|
||||
@ProviderFor(cartUniqueItemCount)
|
||||
const cartUniqueItemCountProvider = CartUniqueItemCountProvider._();
|
||||
|
||||
/// Provider that calculates unique items count in cart
|
||||
|
||||
final class CartUniqueItemCountProvider
|
||||
extends $FunctionalProvider<int, int, int>
|
||||
with $Provider<int> {
|
||||
/// Provider that calculates unique items count in cart
|
||||
const CartUniqueItemCountProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'cartUniqueItemCountProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$cartUniqueItemCountHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<int> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
int create(Ref ref) {
|
||||
return cartUniqueItemCount(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(int value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<int>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$cartUniqueItemCountHash() =>
|
||||
r'51eec092c957d0d4819200fd935115db77c7f8d3';
|
||||
54
lib/features/home/presentation/providers/cart_provider.dart
Normal file
54
lib/features/home/presentation/providers/cart_provider.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import '../../domain/entities/cart_item.dart';
|
||||
|
||||
part 'cart_provider.g.dart';
|
||||
|
||||
/// Provider for shopping cart
|
||||
@riverpod
|
||||
class Cart extends _$Cart {
|
||||
@override
|
||||
Future<List<CartItem>> build() async {
|
||||
// TODO: Implement with repository
|
||||
return [];
|
||||
}
|
||||
|
||||
Future<void> addItem(CartItem item) async {
|
||||
// TODO: Implement add to cart
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
final currentItems = state.value ?? [];
|
||||
return [...currentItems, item];
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> removeItem(String productId) async {
|
||||
// TODO: Implement remove from cart
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
final currentItems = state.value ?? [];
|
||||
return currentItems.where((item) => item.productId != productId).toList();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateQuantity(String productId, int quantity) async {
|
||||
// TODO: Implement update quantity
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
final currentItems = state.value ?? [];
|
||||
return currentItems.map((item) {
|
||||
if (item.productId == productId) {
|
||||
return item.copyWith(quantity: quantity);
|
||||
}
|
||||
return item;
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> clearCart() async {
|
||||
// TODO: Implement clear cart
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
return [];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'cart_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Provider for shopping cart
|
||||
|
||||
@ProviderFor(Cart)
|
||||
const cartProvider = CartProvider._();
|
||||
|
||||
/// Provider for shopping cart
|
||||
final class CartProvider extends $AsyncNotifierProvider<Cart, List<CartItem>> {
|
||||
/// Provider for shopping cart
|
||||
const CartProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'cartProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$cartHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
Cart create() => Cart();
|
||||
}
|
||||
|
||||
String _$cartHash() => r'0136ac2c2a04412a130184e30c01e33a17b0d4db';
|
||||
|
||||
/// Provider for shopping cart
|
||||
|
||||
abstract class _$Cart extends $AsyncNotifier<List<CartItem>> {
|
||||
FutureOr<List<CartItem>> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<AsyncValue<List<CartItem>>, List<CartItem>>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<List<CartItem>>, List<CartItem>>,
|
||||
AsyncValue<List<CartItem>>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'cart_provider.dart';
|
||||
import '../../../settings/presentation/providers/settings_provider.dart';
|
||||
|
||||
part 'cart_total_provider.g.dart';
|
||||
|
||||
/// Cart totals calculation provider
|
||||
@riverpod
|
||||
class CartTotal extends _$CartTotal {
|
||||
@override
|
||||
CartTotalData build() {
|
||||
final itemsAsync = ref.watch(cartProvider);
|
||||
final settingsAsync = ref.watch(settingsProvider);
|
||||
|
||||
final items = itemsAsync.when(
|
||||
data: (data) => data,
|
||||
loading: () => <dynamic>[],
|
||||
error: (_, __) => <dynamic>[],
|
||||
);
|
||||
|
||||
final settings = settingsAsync.when(
|
||||
data: (data) => data,
|
||||
loading: () => null,
|
||||
error: (_, __) => null,
|
||||
);
|
||||
|
||||
// Calculate subtotal
|
||||
final subtotal = items.fold<double>(
|
||||
0.0,
|
||||
(sum, item) => sum + item.lineTotal,
|
||||
);
|
||||
|
||||
// Calculate tax
|
||||
final taxRate = settings?.taxRate ?? 0.0;
|
||||
final tax = subtotal * taxRate;
|
||||
|
||||
// Calculate total
|
||||
final total = subtotal + tax;
|
||||
|
||||
return CartTotalData(
|
||||
subtotal: subtotal,
|
||||
tax: tax,
|
||||
taxRate: taxRate,
|
||||
total: total,
|
||||
itemCount: items.length,
|
||||
);
|
||||
}
|
||||
|
||||
/// Apply discount amount to total
|
||||
double applyDiscount(double discountAmount) {
|
||||
final currentTotal = state.total;
|
||||
return (currentTotal - discountAmount).clamp(0.0, double.infinity);
|
||||
}
|
||||
|
||||
/// Apply discount percentage to total
|
||||
double applyDiscountPercentage(double discountPercent) {
|
||||
final currentTotal = state.total;
|
||||
final discountAmount = currentTotal * (discountPercent / 100);
|
||||
return (currentTotal - discountAmount).clamp(0.0, double.infinity);
|
||||
}
|
||||
}
|
||||
|
||||
/// Cart total data model
|
||||
class CartTotalData {
|
||||
final double subtotal;
|
||||
final double tax;
|
||||
final double taxRate;
|
||||
final double total;
|
||||
final int itemCount;
|
||||
|
||||
const CartTotalData({
|
||||
required this.subtotal,
|
||||
required this.tax,
|
||||
required this.taxRate,
|
||||
required this.total,
|
||||
required this.itemCount,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'CartTotalData(subtotal: $subtotal, tax: $tax, total: $total, items: $itemCount)';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'cart_total_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Cart totals calculation provider
|
||||
|
||||
@ProviderFor(CartTotal)
|
||||
const cartTotalProvider = CartTotalProvider._();
|
||||
|
||||
/// Cart totals calculation provider
|
||||
final class CartTotalProvider
|
||||
extends $NotifierProvider<CartTotal, CartTotalData> {
|
||||
/// Cart totals calculation provider
|
||||
const CartTotalProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'cartTotalProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$cartTotalHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
CartTotal create() => CartTotal();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(CartTotalData value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<CartTotalData>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$cartTotalHash() => r'044f6d4749eec49f9ef4173fc42d149a3841df21';
|
||||
|
||||
/// Cart totals calculation provider
|
||||
|
||||
abstract class _$CartTotal extends $Notifier<CartTotalData> {
|
||||
CartTotalData build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<CartTotalData, CartTotalData>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<CartTotalData, CartTotalData>,
|
||||
CartTotalData,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
4
lib/features/home/presentation/providers/providers.dart
Normal file
4
lib/features/home/presentation/providers/providers.dart
Normal file
@@ -0,0 +1,4 @@
|
||||
/// Export all home/cart providers
|
||||
export 'cart_provider.dart';
|
||||
export 'cart_total_provider.dart';
|
||||
export 'cart_item_count_provider.dart';
|
||||
67
lib/features/home/presentation/widgets/cart_item_card.dart
Normal file
67
lib/features/home/presentation/widgets/cart_item_card.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../domain/entities/cart_item.dart';
|
||||
import '../../../../shared/widgets/price_display.dart';
|
||||
|
||||
/// Cart item card widget
|
||||
class CartItemCard extends StatelessWidget {
|
||||
final CartItem item;
|
||||
final VoidCallback? onRemove;
|
||||
final Function(int)? onQuantityChanged;
|
||||
|
||||
const CartItemCard({
|
||||
super.key,
|
||||
required this.item,
|
||||
this.onRemove,
|
||||
this.onQuantityChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.productName,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
PriceDisplay(price: item.price),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove_circle_outline),
|
||||
onPressed: item.quantity > 1
|
||||
? () => onQuantityChanged?.call(item.quantity - 1)
|
||||
: null,
|
||||
),
|
||||
Text(
|
||||
'${item.quantity}',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add_circle_outline),
|
||||
onPressed: () => onQuantityChanged?.call(item.quantity + 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
onPressed: onRemove,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
128
lib/features/home/presentation/widgets/cart_summary.dart
Normal file
128
lib/features/home/presentation/widgets/cart_summary.dart
Normal file
@@ -0,0 +1,128 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../providers/cart_provider.dart';
|
||||
import '../providers/cart_total_provider.dart';
|
||||
import 'cart_item_card.dart';
|
||||
import '../../../../shared/widgets/price_display.dart';
|
||||
import '../../../../core/widgets/empty_state.dart';
|
||||
|
||||
/// Cart summary widget
|
||||
class CartSummary extends ConsumerWidget {
|
||||
const CartSummary({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final cartAsync = ref.watch(cartProvider);
|
||||
final totalData = ref.watch(cartTotalProvider);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Shopping Cart',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
if (cartAsync.value?.isNotEmpty ?? false)
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
ref.read(cartProvider.notifier).clearCart();
|
||||
},
|
||||
icon: const Icon(Icons.delete_sweep),
|
||||
label: const Text('Clear'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: cartAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) => Center(child: Text('Error: $error')),
|
||||
data: (items) {
|
||||
if (items.isEmpty) {
|
||||
return const EmptyState(
|
||||
message: 'Cart is empty',
|
||||
subMessage: 'Add products to get started',
|
||||
icon: Icons.shopping_cart_outlined,
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
return CartItemCard(
|
||||
item: item,
|
||||
onRemove: () {
|
||||
ref.read(cartProvider.notifier).removeItem(item.productId);
|
||||
},
|
||||
onQuantityChanged: (quantity) {
|
||||
ref.read(cartProvider.notifier).updateQuantity(
|
||||
item.productId,
|
||||
quantity,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Total:',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
PriceDisplay(
|
||||
price: totalData.total,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: (cartAsync.value?.isNotEmpty ?? false)
|
||||
? () {
|
||||
// TODO: Implement checkout
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Icons.payment),
|
||||
label: const Text('Checkout'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.all(16),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
98
lib/features/home/presentation/widgets/product_selector.dart
Normal file
98
lib/features/home/presentation/widgets/product_selector.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../products/presentation/providers/products_provider.dart';
|
||||
import '../../../products/presentation/widgets/product_card.dart';
|
||||
import '../../../products/domain/entities/product.dart';
|
||||
import '../../../../core/widgets/loading_indicator.dart';
|
||||
import '../../../../core/widgets/error_widget.dart';
|
||||
import '../../../../core/widgets/empty_state.dart';
|
||||
|
||||
/// Product selector widget for POS
|
||||
class ProductSelector extends ConsumerWidget {
|
||||
final void Function(Product)? onProductTap;
|
||||
|
||||
const ProductSelector({
|
||||
super.key,
|
||||
this.onProductTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final productsAsync = ref.watch(productsProvider);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Select Products',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: productsAsync.when(
|
||||
loading: () => const LoadingIndicator(
|
||||
message: 'Loading products...',
|
||||
),
|
||||
error: (error, stack) => ErrorDisplay(
|
||||
message: error.toString(),
|
||||
onRetry: () => ref.refresh(productsProvider),
|
||||
),
|
||||
data: (products) {
|
||||
if (products.isEmpty) {
|
||||
return const EmptyState(
|
||||
message: 'No products available',
|
||||
subMessage: 'Add products to start selling',
|
||||
icon: Icons.inventory_2_outlined,
|
||||
);
|
||||
}
|
||||
|
||||
// Filter only available products for POS
|
||||
final availableProducts =
|
||||
products.where((p) => p.isAvailable).toList();
|
||||
|
||||
if (availableProducts.isEmpty) {
|
||||
return const EmptyState(
|
||||
message: 'No products available',
|
||||
subMessage: 'All products are currently unavailable',
|
||||
icon: Icons.inventory_2_outlined,
|
||||
);
|
||||
}
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// Determine grid columns based on width
|
||||
int crossAxisCount = 2;
|
||||
if (constraints.maxWidth > 800) {
|
||||
crossAxisCount = 4;
|
||||
} else if (constraints.maxWidth > 600) {
|
||||
crossAxisCount = 3;
|
||||
}
|
||||
|
||||
return GridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
childAspectRatio: 0.75,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
),
|
||||
itemCount: availableProducts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final product = availableProducts[index];
|
||||
return GestureDetector(
|
||||
onTap: () => onProductTap?.call(product),
|
||||
child: ProductCard(product: product),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
5
lib/features/home/presentation/widgets/widgets.dart
Normal file
5
lib/features/home/presentation/widgets/widgets.dart
Normal file
@@ -0,0 +1,5 @@
|
||||
// Home/Cart Feature Widgets
|
||||
export 'cart_item_card.dart';
|
||||
export 'cart_summary.dart';
|
||||
|
||||
// This file provides a central export point for all home/cart widgets
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import '../models/product_model.dart';
|
||||
|
||||
/// Product local data source using Hive
|
||||
abstract class ProductLocalDataSource {
|
||||
Future<List<ProductModel>> getAllProducts();
|
||||
Future<ProductModel?> getProductById(String id);
|
||||
Future<void> cacheProducts(List<ProductModel> products);
|
||||
Future<void> clearProducts();
|
||||
}
|
||||
|
||||
class ProductLocalDataSourceImpl implements ProductLocalDataSource {
|
||||
final Box<ProductModel> box;
|
||||
|
||||
ProductLocalDataSourceImpl(this.box);
|
||||
|
||||
@override
|
||||
Future<List<ProductModel>> getAllProducts() async {
|
||||
return box.values.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ProductModel?> getProductById(String id) async {
|
||||
return box.get(id);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> cacheProducts(List<ProductModel> products) async {
|
||||
final productMap = {for (var p in products) p.id: p};
|
||||
await box.putAll(productMap);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearProducts() async {
|
||||
await box.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import '../models/product_model.dart';
|
||||
import '../../../../core/network/dio_client.dart';
|
||||
import '../../../../core/constants/api_constants.dart';
|
||||
|
||||
/// Product remote data source using API
|
||||
abstract class ProductRemoteDataSource {
|
||||
Future<List<ProductModel>> getAllProducts();
|
||||
Future<ProductModel> getProductById(String id);
|
||||
Future<List<ProductModel>> searchProducts(String query);
|
||||
}
|
||||
|
||||
class ProductRemoteDataSourceImpl implements ProductRemoteDataSource {
|
||||
final DioClient client;
|
||||
|
||||
ProductRemoteDataSourceImpl(this.client);
|
||||
|
||||
@override
|
||||
Future<List<ProductModel>> getAllProducts() async {
|
||||
final response = await client.get(ApiConstants.products);
|
||||
final List<dynamic> data = response.data['products'] ?? [];
|
||||
return data.map((json) => ProductModel.fromJson(json)).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ProductModel> getProductById(String id) async {
|
||||
final response = await client.get(ApiConstants.productById(id));
|
||||
return ProductModel.fromJson(response.data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ProductModel>> searchProducts(String query) async {
|
||||
final response = await client.get(
|
||||
ApiConstants.searchProducts,
|
||||
queryParameters: {'q': query},
|
||||
);
|
||||
final List<dynamic> data = response.data['products'] ?? [];
|
||||
return data.map((json) => ProductModel.fromJson(json)).toList();
|
||||
}
|
||||
}
|
||||
115
lib/features/products/data/models/product_model.dart
Normal file
115
lib/features/products/data/models/product_model.dart
Normal file
@@ -0,0 +1,115 @@
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import '../../domain/entities/product.dart';
|
||||
import '../../../../core/constants/storage_constants.dart';
|
||||
|
||||
part 'product_model.g.dart';
|
||||
|
||||
@HiveType(typeId: StorageConstants.productTypeId)
|
||||
class ProductModel extends HiveObject {
|
||||
@HiveField(0)
|
||||
final String id;
|
||||
|
||||
@HiveField(1)
|
||||
final String name;
|
||||
|
||||
@HiveField(2)
|
||||
final String description;
|
||||
|
||||
@HiveField(3)
|
||||
final double price;
|
||||
|
||||
@HiveField(4)
|
||||
final String? imageUrl;
|
||||
|
||||
@HiveField(5)
|
||||
final String categoryId;
|
||||
|
||||
@HiveField(6)
|
||||
final int stockQuantity;
|
||||
|
||||
@HiveField(7)
|
||||
final bool isAvailable;
|
||||
|
||||
@HiveField(8)
|
||||
final DateTime createdAt;
|
||||
|
||||
@HiveField(9)
|
||||
final DateTime updatedAt;
|
||||
|
||||
ProductModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.price,
|
||||
this.imageUrl,
|
||||
required this.categoryId,
|
||||
required this.stockQuantity,
|
||||
required this.isAvailable,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
/// Convert to domain entity
|
||||
Product toEntity() {
|
||||
return Product(
|
||||
id: id,
|
||||
name: name,
|
||||
description: description,
|
||||
price: price,
|
||||
imageUrl: imageUrl,
|
||||
categoryId: categoryId,
|
||||
stockQuantity: stockQuantity,
|
||||
isAvailable: isAvailable,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create from domain entity
|
||||
factory ProductModel.fromEntity(Product product) {
|
||||
return ProductModel(
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
description: product.description,
|
||||
price: product.price,
|
||||
imageUrl: product.imageUrl,
|
||||
categoryId: product.categoryId,
|
||||
stockQuantity: product.stockQuantity,
|
||||
isAvailable: product.isAvailable,
|
||||
createdAt: product.createdAt,
|
||||
updatedAt: product.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create from JSON
|
||||
factory ProductModel.fromJson(Map<String, dynamic> json) {
|
||||
return ProductModel(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String,
|
||||
price: (json['price'] as num).toDouble(),
|
||||
imageUrl: json['imageUrl'] as String?,
|
||||
categoryId: json['categoryId'] as String,
|
||||
stockQuantity: json['stockQuantity'] as int,
|
||||
isAvailable: json['isAvailable'] as bool,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'description': description,
|
||||
'price': price,
|
||||
'imageUrl': imageUrl,
|
||||
'categoryId': categoryId,
|
||||
'stockQuantity': stockQuantity,
|
||||
'isAvailable': isAvailable,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'updatedAt': updatedAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
68
lib/features/products/data/models/product_model.g.dart
Normal file
68
lib/features/products/data/models/product_model.g.dart
Normal file
@@ -0,0 +1,68 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'product_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class ProductModelAdapter extends TypeAdapter<ProductModel> {
|
||||
@override
|
||||
final typeId = 0;
|
||||
|
||||
@override
|
||||
ProductModel read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return ProductModel(
|
||||
id: fields[0] as String,
|
||||
name: fields[1] as String,
|
||||
description: fields[2] as String,
|
||||
price: (fields[3] as num).toDouble(),
|
||||
imageUrl: fields[4] as String?,
|
||||
categoryId: fields[5] as String,
|
||||
stockQuantity: (fields[6] as num).toInt(),
|
||||
isAvailable: fields[7] as bool,
|
||||
createdAt: fields[8] as DateTime,
|
||||
updatedAt: fields[9] as DateTime,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, ProductModel obj) {
|
||||
writer
|
||||
..writeByte(10)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
..write(obj.name)
|
||||
..writeByte(2)
|
||||
..write(obj.description)
|
||||
..writeByte(3)
|
||||
..write(obj.price)
|
||||
..writeByte(4)
|
||||
..write(obj.imageUrl)
|
||||
..writeByte(5)
|
||||
..write(obj.categoryId)
|
||||
..writeByte(6)
|
||||
..write(obj.stockQuantity)
|
||||
..writeByte(7)
|
||||
..write(obj.isAvailable)
|
||||
..writeByte(8)
|
||||
..write(obj.createdAt)
|
||||
..writeByte(9)
|
||||
..write(obj.updatedAt);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ProductModelAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../domain/entities/product.dart';
|
||||
import '../../domain/repositories/product_repository.dart';
|
||||
import '../datasources/product_local_datasource.dart';
|
||||
import '../datasources/product_remote_datasource.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../../../../core/errors/exceptions.dart';
|
||||
|
||||
class ProductRepositoryImpl implements ProductRepository {
|
||||
final ProductLocalDataSource localDataSource;
|
||||
final ProductRemoteDataSource remoteDataSource;
|
||||
|
||||
ProductRepositoryImpl({
|
||||
required this.localDataSource,
|
||||
required this.remoteDataSource,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<Product>>> getAllProducts() async {
|
||||
try {
|
||||
final products = await localDataSource.getAllProducts();
|
||||
return Right(products.map((model) => model.toEntity()).toList());
|
||||
} on CacheException catch (e) {
|
||||
return Left(CacheFailure(e.message));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<Product>>> getProductsByCategory(String categoryId) async {
|
||||
try {
|
||||
final allProducts = await localDataSource.getAllProducts();
|
||||
final filtered = allProducts.where((p) => p.categoryId == categoryId).toList();
|
||||
return Right(filtered.map((model) => model.toEntity()).toList());
|
||||
} on CacheException catch (e) {
|
||||
return Left(CacheFailure(e.message));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<Product>>> searchProducts(String query) async {
|
||||
try {
|
||||
final allProducts = await localDataSource.getAllProducts();
|
||||
final filtered = allProducts.where((p) =>
|
||||
p.name.toLowerCase().contains(query.toLowerCase()) ||
|
||||
p.description.toLowerCase().contains(query.toLowerCase())
|
||||
).toList();
|
||||
return Right(filtered.map((model) => model.toEntity()).toList());
|
||||
} on CacheException catch (e) {
|
||||
return Left(CacheFailure(e.message));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Product>> getProductById(String id) async {
|
||||
try {
|
||||
final product = await localDataSource.getProductById(id);
|
||||
if (product == null) {
|
||||
return Left(NotFoundFailure('Product not found'));
|
||||
}
|
||||
return Right(product.toEntity());
|
||||
} on CacheException catch (e) {
|
||||
return Left(CacheFailure(e.message));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<Product>>> syncProducts() async {
|
||||
try {
|
||||
final products = await remoteDataSource.getAllProducts();
|
||||
await localDataSource.cacheProducts(products);
|
||||
return Right(products.map((model) => model.toEntity()).toList());
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on NetworkException catch (e) {
|
||||
return Left(NetworkFailure(e.message));
|
||||
}
|
||||
}
|
||||
}
|
||||
42
lib/features/products/domain/entities/product.dart
Normal file
42
lib/features/products/domain/entities/product.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Product domain entity
|
||||
class Product extends Equatable {
|
||||
final String id;
|
||||
final String name;
|
||||
final String description;
|
||||
final double price;
|
||||
final String? imageUrl;
|
||||
final String categoryId;
|
||||
final int stockQuantity;
|
||||
final bool isAvailable;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
const Product({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.price,
|
||||
this.imageUrl,
|
||||
required this.categoryId,
|
||||
required this.stockQuantity,
|
||||
required this.isAvailable,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
price,
|
||||
imageUrl,
|
||||
categoryId,
|
||||
stockQuantity,
|
||||
isAvailable,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../entities/product.dart';
|
||||
|
||||
/// Product repository interface
|
||||
abstract class ProductRepository {
|
||||
/// Get all products from cache
|
||||
Future<Either<Failure, List<Product>>> getAllProducts();
|
||||
|
||||
/// Get products by category
|
||||
Future<Either<Failure, List<Product>>> getProductsByCategory(String categoryId);
|
||||
|
||||
/// Search products
|
||||
Future<Either<Failure, List<Product>>> searchProducts(String query);
|
||||
|
||||
/// Get product by ID
|
||||
Future<Either<Failure, Product>> getProductById(String id);
|
||||
|
||||
/// Sync products from remote
|
||||
Future<Either<Failure, List<Product>>> syncProducts();
|
||||
}
|
||||
15
lib/features/products/domain/usecases/get_all_products.dart
Normal file
15
lib/features/products/domain/usecases/get_all_products.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../entities/product.dart';
|
||||
import '../repositories/product_repository.dart';
|
||||
|
||||
/// Use case to get all products
|
||||
class GetAllProducts {
|
||||
final ProductRepository repository;
|
||||
|
||||
GetAllProducts(this.repository);
|
||||
|
||||
Future<Either<Failure, List<Product>>> call() async {
|
||||
return await repository.getAllProducts();
|
||||
}
|
||||
}
|
||||
15
lib/features/products/domain/usecases/search_products.dart
Normal file
15
lib/features/products/domain/usecases/search_products.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../entities/product.dart';
|
||||
import '../repositories/product_repository.dart';
|
||||
|
||||
/// Use case to search products
|
||||
class SearchProducts {
|
||||
final ProductRepository repository;
|
||||
|
||||
SearchProducts(this.repository);
|
||||
|
||||
Future<Either<Failure, List<Product>>> call(String query) async {
|
||||
return await repository.searchProducts(query);
|
||||
}
|
||||
}
|
||||
200
lib/features/products/presentation/pages/products_page.dart
Normal file
200
lib/features/products/presentation/pages/products_page.dart
Normal file
@@ -0,0 +1,200 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../widgets/product_grid.dart';
|
||||
import '../widgets/product_search_bar.dart';
|
||||
import '../providers/products_provider.dart';
|
||||
import '../providers/selected_category_provider.dart' as product_providers;
|
||||
import '../providers/filtered_products_provider.dart';
|
||||
import '../../domain/entities/product.dart';
|
||||
import '../../../categories/presentation/providers/categories_provider.dart';
|
||||
|
||||
/// Products page - displays all products in a grid
|
||||
class ProductsPage extends ConsumerStatefulWidget {
|
||||
const ProductsPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ProductsPage> createState() => _ProductsPageState();
|
||||
}
|
||||
|
||||
class _ProductsPageState extends ConsumerState<ProductsPage> {
|
||||
ProductSortOption _sortOption = ProductSortOption.nameAsc;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final categoriesAsync = ref.watch(categoriesProvider);
|
||||
final selectedCategory = ref.watch(product_providers.selectedCategoryProvider);
|
||||
final productsAsync = ref.watch(productsProvider);
|
||||
|
||||
// Get filtered products from the provider
|
||||
final filteredProducts = productsAsync.when(
|
||||
data: (products) => products,
|
||||
loading: () => <Product>[],
|
||||
error: (_, __) => <Product>[],
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Products'),
|
||||
actions: [
|
||||
// Sort button
|
||||
PopupMenuButton<ProductSortOption>(
|
||||
icon: const Icon(Icons.sort),
|
||||
tooltip: 'Sort products',
|
||||
onSelected: (option) {
|
||||
setState(() {
|
||||
_sortOption = option;
|
||||
});
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
value: ProductSortOption.nameAsc,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.sort_by_alpha),
|
||||
SizedBox(width: 8),
|
||||
Text('Name (A-Z)'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: ProductSortOption.nameDesc,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.sort_by_alpha),
|
||||
SizedBox(width: 8),
|
||||
Text('Name (Z-A)'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: ProductSortOption.priceAsc,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.attach_money),
|
||||
SizedBox(width: 8),
|
||||
Text('Price (Low to High)'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: ProductSortOption.priceDesc,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.attach_money),
|
||||
SizedBox(width: 8),
|
||||
Text('Price (High to Low)'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: ProductSortOption.newest,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.access_time),
|
||||
SizedBox(width: 8),
|
||||
Text('Newest First'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: ProductSortOption.oldest,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.access_time),
|
||||
SizedBox(width: 8),
|
||||
Text('Oldest First'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(120),
|
||||
child: Column(
|
||||
children: [
|
||||
// Search bar
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: ProductSearchBar(),
|
||||
),
|
||||
// Category filter chips
|
||||
categoriesAsync.when(
|
||||
loading: () => const SizedBox.shrink(),
|
||||
error: (_, __) => const SizedBox.shrink(),
|
||||
data: (categories) {
|
||||
if (categories.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return SizedBox(
|
||||
height: 50,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
children: [
|
||||
// All categories chip
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: FilterChip(
|
||||
label: const Text('All'),
|
||||
selected: selectedCategory == null,
|
||||
onSelected: (_) {
|
||||
ref
|
||||
.read(product_providers.selectedCategoryProvider.notifier)
|
||||
.clearSelection();
|
||||
},
|
||||
),
|
||||
),
|
||||
// Category chips
|
||||
...categories.map(
|
||||
(category) => Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: FilterChip(
|
||||
label: Text(category.name),
|
||||
selected: selectedCategory == category.id,
|
||||
onSelected: (_) {
|
||||
ref
|
||||
.read(product_providers.selectedCategoryProvider.notifier)
|
||||
.selectCategory(category.id);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await ref.refresh(productsProvider.future);
|
||||
await ref.refresh(categoriesProvider.future);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
// Results count
|
||||
if (filteredProducts.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'${filteredProducts.length} product${filteredProducts.length == 1 ? '' : 's'} found',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Product grid
|
||||
Expanded(
|
||||
child: ProductGrid(
|
||||
sortOption: _sortOption,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import '../../domain/entities/product.dart';
|
||||
import 'products_provider.dart';
|
||||
import 'search_query_provider.dart' as search_providers;
|
||||
import 'selected_category_provider.dart';
|
||||
|
||||
part 'filtered_products_provider.g.dart';
|
||||
|
||||
/// Filtered products provider
|
||||
/// Combines products, search query, and category filter to provide filtered results
|
||||
@riverpod
|
||||
class FilteredProducts extends _$FilteredProducts {
|
||||
@override
|
||||
List<Product> build() {
|
||||
// Watch all products
|
||||
final productsAsync = ref.watch(productsProvider);
|
||||
final products = productsAsync.when(
|
||||
data: (data) => data,
|
||||
loading: () => <Product>[],
|
||||
error: (_, __) => <Product>[],
|
||||
);
|
||||
|
||||
// Watch search query
|
||||
final searchQuery = ref.watch(search_providers.searchQueryProvider);
|
||||
|
||||
// Watch selected category
|
||||
final selectedCategory = ref.watch(selectedCategoryProvider);
|
||||
|
||||
// Apply filters
|
||||
return _applyFilters(products, searchQuery, selectedCategory);
|
||||
}
|
||||
|
||||
/// Apply search and category filters to products
|
||||
List<Product> _applyFilters(
|
||||
List<Product> products,
|
||||
String searchQuery,
|
||||
String? categoryId,
|
||||
) {
|
||||
var filtered = products;
|
||||
|
||||
// Filter by category if selected
|
||||
if (categoryId != null) {
|
||||
filtered = filtered.where((p) => p.categoryId == categoryId).toList();
|
||||
}
|
||||
|
||||
// Filter by search query if present
|
||||
if (searchQuery.isNotEmpty) {
|
||||
final lowerQuery = searchQuery.toLowerCase();
|
||||
filtered = filtered.where((p) {
|
||||
return p.name.toLowerCase().contains(lowerQuery) ||
|
||||
p.description.toLowerCase().contains(lowerQuery);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/// Get available products count
|
||||
int get availableCount => state.where((p) => p.isAvailable).length;
|
||||
|
||||
/// Get out of stock products count
|
||||
int get outOfStockCount => state.where((p) => !p.isAvailable).length;
|
||||
}
|
||||
|
||||
/// Provider for sorted products
|
||||
/// Adds sorting capability on top of filtered products
|
||||
@riverpod
|
||||
class SortedProducts extends _$SortedProducts {
|
||||
@override
|
||||
List<Product> build(ProductSortOption sortOption) {
|
||||
final filteredProducts = ref.watch(filteredProductsProvider);
|
||||
|
||||
return _sortProducts(filteredProducts, sortOption);
|
||||
}
|
||||
|
||||
List<Product> _sortProducts(List<Product> products, ProductSortOption option) {
|
||||
final sorted = List<Product>.from(products);
|
||||
|
||||
switch (option) {
|
||||
case ProductSortOption.nameAsc:
|
||||
sorted.sort((a, b) => a.name.compareTo(b.name));
|
||||
break;
|
||||
case ProductSortOption.nameDesc:
|
||||
sorted.sort((a, b) => b.name.compareTo(a.name));
|
||||
break;
|
||||
case ProductSortOption.priceAsc:
|
||||
sorted.sort((a, b) => a.price.compareTo(b.price));
|
||||
break;
|
||||
case ProductSortOption.priceDesc:
|
||||
sorted.sort((a, b) => b.price.compareTo(a.price));
|
||||
break;
|
||||
case ProductSortOption.newest:
|
||||
sorted.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
break;
|
||||
case ProductSortOption.oldest:
|
||||
sorted.sort((a, b) => a.createdAt.compareTo(b.createdAt));
|
||||
break;
|
||||
}
|
||||
|
||||
return sorted;
|
||||
}
|
||||
}
|
||||
|
||||
/// Product sort options
|
||||
enum ProductSortOption {
|
||||
nameAsc,
|
||||
nameDesc,
|
||||
priceAsc,
|
||||
priceDesc,
|
||||
newest,
|
||||
oldest,
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'filtered_products_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Filtered products provider
|
||||
/// Combines products, search query, and category filter to provide filtered results
|
||||
|
||||
@ProviderFor(FilteredProducts)
|
||||
const filteredProductsProvider = FilteredProductsProvider._();
|
||||
|
||||
/// Filtered products provider
|
||||
/// Combines products, search query, and category filter to provide filtered results
|
||||
final class FilteredProductsProvider
|
||||
extends $NotifierProvider<FilteredProducts, List<Product>> {
|
||||
/// Filtered products provider
|
||||
/// Combines products, search query, and category filter to provide filtered results
|
||||
const FilteredProductsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'filteredProductsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$filteredProductsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
FilteredProducts create() => FilteredProducts();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(List<Product> value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<List<Product>>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$filteredProductsHash() => r'04d66ed1cb868008cf3e6aba6571f7928a48e814';
|
||||
|
||||
/// Filtered products provider
|
||||
/// Combines products, search query, and category filter to provide filtered results
|
||||
|
||||
abstract class _$FilteredProducts extends $Notifier<List<Product>> {
|
||||
List<Product> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<List<Product>, List<Product>>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<List<Product>, List<Product>>,
|
||||
List<Product>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider for sorted products
|
||||
/// Adds sorting capability on top of filtered products
|
||||
|
||||
@ProviderFor(SortedProducts)
|
||||
const sortedProductsProvider = SortedProductsFamily._();
|
||||
|
||||
/// Provider for sorted products
|
||||
/// Adds sorting capability on top of filtered products
|
||||
final class SortedProductsProvider
|
||||
extends $NotifierProvider<SortedProducts, List<Product>> {
|
||||
/// Provider for sorted products
|
||||
/// Adds sorting capability on top of filtered products
|
||||
const SortedProductsProvider._({
|
||||
required SortedProductsFamily super.from,
|
||||
required ProductSortOption super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'sortedProductsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$sortedProductsHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'sortedProductsProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
SortedProducts create() => SortedProducts();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(List<Product> value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<List<Product>>(value),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is SortedProductsProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$sortedProductsHash() => r'653f1e9af8c188631dadbfe9ed7d944c6876d2d3';
|
||||
|
||||
/// Provider for sorted products
|
||||
/// Adds sorting capability on top of filtered products
|
||||
|
||||
final class SortedProductsFamily extends $Family
|
||||
with
|
||||
$ClassFamilyOverride<
|
||||
SortedProducts,
|
||||
List<Product>,
|
||||
List<Product>,
|
||||
List<Product>,
|
||||
ProductSortOption
|
||||
> {
|
||||
const SortedProductsFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'sortedProductsProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
/// Provider for sorted products
|
||||
/// Adds sorting capability on top of filtered products
|
||||
|
||||
SortedProductsProvider call(ProductSortOption sortOption) =>
|
||||
SortedProductsProvider._(argument: sortOption, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'sortedProductsProvider';
|
||||
}
|
||||
|
||||
/// Provider for sorted products
|
||||
/// Adds sorting capability on top of filtered products
|
||||
|
||||
abstract class _$SortedProducts extends $Notifier<List<Product>> {
|
||||
late final _$args = ref.$arg as ProductSortOption;
|
||||
ProductSortOption get sortOption => _$args;
|
||||
|
||||
List<Product> build(ProductSortOption sortOption);
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build(_$args);
|
||||
final ref = this.ref as $Ref<List<Product>, List<Product>>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<List<Product>, List<Product>>,
|
||||
List<Product>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import '../../domain/entities/product.dart';
|
||||
|
||||
part 'products_provider.g.dart';
|
||||
|
||||
/// Provider for products list
|
||||
@riverpod
|
||||
class Products extends _$Products {
|
||||
@override
|
||||
Future<List<Product>> build() async {
|
||||
// TODO: Implement with repository
|
||||
return [];
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
// TODO: Implement refresh logic
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
// Fetch products from repository
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> syncProducts() async {
|
||||
// TODO: Implement sync logic with remote data source
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
// Sync products from API
|
||||
return [];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider for search query
|
||||
@riverpod
|
||||
class SearchQuery extends _$SearchQuery {
|
||||
@override
|
||||
String build() => '';
|
||||
|
||||
void setQuery(String query) {
|
||||
state = query;
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider for filtered products
|
||||
@riverpod
|
||||
List<Product> filteredProducts(Ref ref) {
|
||||
final products = ref.watch(productsProvider).value ?? [];
|
||||
final query = ref.watch(searchQueryProvider);
|
||||
|
||||
if (query.isEmpty) return products;
|
||||
|
||||
return products.where((p) =>
|
||||
p.name.toLowerCase().contains(query.toLowerCase()) ||
|
||||
p.description.toLowerCase().contains(query.toLowerCase())
|
||||
).toList();
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'products_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Provider for products list
|
||||
|
||||
@ProviderFor(Products)
|
||||
const productsProvider = ProductsProvider._();
|
||||
|
||||
/// Provider for products list
|
||||
final class ProductsProvider
|
||||
extends $AsyncNotifierProvider<Products, List<Product>> {
|
||||
/// Provider for products list
|
||||
const ProductsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'productsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$productsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
Products create() => Products();
|
||||
}
|
||||
|
||||
String _$productsHash() => r'9e1d3aaa1d9cf0b4ff03fdfaf4512a7a15336d51';
|
||||
|
||||
/// Provider for products list
|
||||
|
||||
abstract class _$Products extends $AsyncNotifier<List<Product>> {
|
||||
FutureOr<List<Product>> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<AsyncValue<List<Product>>, List<Product>>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<List<Product>>, List<Product>>,
|
||||
AsyncValue<List<Product>>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider for search query
|
||||
|
||||
@ProviderFor(SearchQuery)
|
||||
const searchQueryProvider = SearchQueryProvider._();
|
||||
|
||||
/// Provider for search query
|
||||
final class SearchQueryProvider extends $NotifierProvider<SearchQuery, String> {
|
||||
/// Provider for search query
|
||||
const SearchQueryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'searchQueryProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$searchQueryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
SearchQuery create() => SearchQuery();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(String value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<String>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$searchQueryHash() => r'2c146927785523a0ddf51b23b777a9be4afdc092';
|
||||
|
||||
/// Provider for search query
|
||||
|
||||
abstract class _$SearchQuery extends $Notifier<String> {
|
||||
String build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<String, String>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<String, String>,
|
||||
String,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider for filtered products
|
||||
|
||||
@ProviderFor(filteredProducts)
|
||||
const filteredProductsProvider = FilteredProductsProvider._();
|
||||
|
||||
/// Provider for filtered products
|
||||
|
||||
final class FilteredProductsProvider
|
||||
extends $FunctionalProvider<List<Product>, List<Product>, List<Product>>
|
||||
with $Provider<List<Product>> {
|
||||
/// Provider for filtered products
|
||||
const FilteredProductsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'filteredProductsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$filteredProductsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<List<Product>> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
List<Product> create(Ref ref) {
|
||||
return filteredProducts(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(List<Product> value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<List<Product>>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$filteredProductsHash() => r'e4e0c549c454576fc599713a5237435a8dd4b277';
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'search_query_provider.g.dart';
|
||||
|
||||
/// Search query state provider
|
||||
/// Manages the current search query string for product filtering
|
||||
@riverpod
|
||||
class SearchQuery extends _$SearchQuery {
|
||||
@override
|
||||
String build() {
|
||||
// Initialize with empty search query
|
||||
return '';
|
||||
}
|
||||
|
||||
/// Update search query
|
||||
void setQuery(String query) {
|
||||
state = query.trim();
|
||||
}
|
||||
|
||||
/// Clear search query
|
||||
void clear() {
|
||||
state = '';
|
||||
}
|
||||
|
||||
/// Check if search is active
|
||||
bool get isSearching => state.isNotEmpty;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'search_query_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Search query state provider
|
||||
/// Manages the current search query string for product filtering
|
||||
|
||||
@ProviderFor(SearchQuery)
|
||||
const searchQueryProvider = SearchQueryProvider._();
|
||||
|
||||
/// Search query state provider
|
||||
/// Manages the current search query string for product filtering
|
||||
final class SearchQueryProvider extends $NotifierProvider<SearchQuery, String> {
|
||||
/// Search query state provider
|
||||
/// Manages the current search query string for product filtering
|
||||
const SearchQueryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'searchQueryProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$searchQueryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
SearchQuery create() => SearchQuery();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(String value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<String>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$searchQueryHash() => r'62191c640ca9424065338a26c1af5c4695a46ef5';
|
||||
|
||||
/// Search query state provider
|
||||
/// Manages the current search query string for product filtering
|
||||
|
||||
abstract class _$SearchQuery extends $Notifier<String> {
|
||||
String build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<String, String>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<String, String>,
|
||||
String,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'selected_category_provider.g.dart';
|
||||
|
||||
/// Selected category state provider
|
||||
/// Manages the currently selected category for product filtering
|
||||
@riverpod
|
||||
class SelectedCategory extends _$SelectedCategory {
|
||||
@override
|
||||
String? build() {
|
||||
// Initialize with no category selected (show all products)
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Select a category
|
||||
void selectCategory(String? categoryId) {
|
||||
state = categoryId;
|
||||
}
|
||||
|
||||
/// Clear category selection (show all products)
|
||||
void clearSelection() {
|
||||
state = null;
|
||||
}
|
||||
|
||||
/// Check if a category is selected
|
||||
bool get hasSelection => state != null;
|
||||
|
||||
/// Check if specific category is selected
|
||||
bool isSelected(String categoryId) => state == categoryId;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'selected_category_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Selected category state provider
|
||||
/// Manages the currently selected category for product filtering
|
||||
|
||||
@ProviderFor(SelectedCategory)
|
||||
const selectedCategoryProvider = SelectedCategoryProvider._();
|
||||
|
||||
/// Selected category state provider
|
||||
/// Manages the currently selected category for product filtering
|
||||
final class SelectedCategoryProvider
|
||||
extends $NotifierProvider<SelectedCategory, String?> {
|
||||
/// Selected category state provider
|
||||
/// Manages the currently selected category for product filtering
|
||||
const SelectedCategoryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'selectedCategoryProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$selectedCategoryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
SelectedCategory create() => SelectedCategory();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(String? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<String?>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$selectedCategoryHash() => r'73e33604e69d2e9f9127f21e6784c5fe8ddf4869';
|
||||
|
||||
/// Selected category state provider
|
||||
/// Manages the currently selected category for product filtering
|
||||
|
||||
abstract class _$SelectedCategory extends $Notifier<String?> {
|
||||
String? build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<String?, String?>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<String?, String?>,
|
||||
String?,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
81
lib/features/products/presentation/widgets/product_card.dart
Normal file
81
lib/features/products/presentation/widgets/product_card.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import '../../domain/entities/product.dart';
|
||||
import '../../../../shared/widgets/price_display.dart';
|
||||
|
||||
/// Product card widget
|
||||
class ProductCard extends StatelessWidget {
|
||||
final Product product;
|
||||
|
||||
const ProductCard({
|
||||
super.key,
|
||||
required this.product,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
// TODO: Navigate to product details or add to cart
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: product.imageUrl != null
|
||||
? CachedNetworkImage(
|
||||
imageUrl: product.imageUrl!,
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
placeholder: (context, url) => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
errorWidget: (context, url, error) => Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
child: Icon(
|
||||
Icons.image_not_supported,
|
||||
size: 48,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
child: Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 48,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
product.name,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
PriceDisplay(price: product.price),
|
||||
if (product.stockQuantity < 5)
|
||||
Text(
|
||||
'Low stock: ${product.stockQuantity}',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
86
lib/features/products/presentation/widgets/product_grid.dart
Normal file
86
lib/features/products/presentation/widgets/product_grid.dart
Normal file
@@ -0,0 +1,86 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../providers/filtered_products_provider.dart';
|
||||
import 'product_card.dart';
|
||||
import '../../../../core/widgets/empty_state.dart';
|
||||
|
||||
/// Product grid widget
|
||||
class ProductGrid extends ConsumerWidget {
|
||||
final ProductSortOption sortOption;
|
||||
|
||||
const ProductGrid({
|
||||
super.key,
|
||||
this.sortOption = ProductSortOption.nameAsc,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final filteredProducts = ref.watch(filteredProductsProvider);
|
||||
|
||||
// Apply sorting
|
||||
final sortedProducts = _sortProducts(filteredProducts, sortOption);
|
||||
|
||||
if (sortedProducts.isEmpty) {
|
||||
return const EmptyState(
|
||||
message: 'No products found',
|
||||
subMessage: 'Try adjusting your filters',
|
||||
icon: Icons.inventory_2_outlined,
|
||||
);
|
||||
}
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// Determine grid columns based on width
|
||||
int crossAxisCount = 2;
|
||||
if (constraints.maxWidth > 1200) {
|
||||
crossAxisCount = 4;
|
||||
} else if (constraints.maxWidth > 800) {
|
||||
crossAxisCount = 3;
|
||||
}
|
||||
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
childAspectRatio: 0.75,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
),
|
||||
itemCount: sortedProducts.length,
|
||||
itemBuilder: (context, index) {
|
||||
return RepaintBoundary(
|
||||
child: ProductCard(product: sortedProducts[index]),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<dynamic> _sortProducts(List<dynamic> products, ProductSortOption option) {
|
||||
final sorted = List.from(products);
|
||||
|
||||
switch (option) {
|
||||
case ProductSortOption.nameAsc:
|
||||
sorted.sort((a, b) => a.name.compareTo(b.name));
|
||||
break;
|
||||
case ProductSortOption.nameDesc:
|
||||
sorted.sort((a, b) => b.name.compareTo(a.name));
|
||||
break;
|
||||
case ProductSortOption.priceAsc:
|
||||
sorted.sort((a, b) => a.price.compareTo(b.price));
|
||||
break;
|
||||
case ProductSortOption.priceDesc:
|
||||
sorted.sort((a, b) => b.price.compareTo(a.price));
|
||||
break;
|
||||
case ProductSortOption.newest:
|
||||
sorted.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
break;
|
||||
case ProductSortOption.oldest:
|
||||
sorted.sort((a, b) => a.createdAt.compareTo(b.createdAt));
|
||||
break;
|
||||
}
|
||||
|
||||
return sorted;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../providers/products_provider.dart';
|
||||
|
||||
/// Product search bar widget
|
||||
class ProductSearchBar extends ConsumerStatefulWidget {
|
||||
const ProductSearchBar({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ProductSearchBar> createState() => _ProductSearchBarState();
|
||||
}
|
||||
|
||||
class _ProductSearchBarState extends ConsumerState<ProductSearchBar> {
|
||||
final _controller = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextField(
|
||||
controller: _controller,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search products...',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: _controller.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_controller.clear();
|
||||
ref.read(searchQueryProvider.notifier).setQuery('');
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
onChanged: (value) {
|
||||
ref.read(searchQueryProvider.notifier).setQuery(value);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
6
lib/features/products/presentation/widgets/widgets.dart
Normal file
6
lib/features/products/presentation/widgets/widgets.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
// Product Feature Widgets
|
||||
export 'product_card.dart';
|
||||
export 'product_grid.dart';
|
||||
export 'product_search_bar.dart';
|
||||
|
||||
// This file provides a central export point for all product widgets
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import '../models/app_settings_model.dart';
|
||||
import '../../../../core/constants/storage_constants.dart';
|
||||
import '../../../../core/constants/app_constants.dart';
|
||||
|
||||
/// Settings local data source using Hive
|
||||
abstract class SettingsLocalDataSource {
|
||||
Future<AppSettingsModel> getSettings();
|
||||
Future<void> updateSettings(AppSettingsModel settings);
|
||||
}
|
||||
|
||||
class SettingsLocalDataSourceImpl implements SettingsLocalDataSource {
|
||||
final Box<AppSettingsModel> box;
|
||||
|
||||
SettingsLocalDataSourceImpl(this.box);
|
||||
|
||||
@override
|
||||
Future<AppSettingsModel> getSettings() async {
|
||||
var settings = box.get(StorageConstants.settingsKey);
|
||||
|
||||
// Return default settings if not found
|
||||
if (settings == null) {
|
||||
settings = AppSettingsModel(
|
||||
themeModeString: 'system',
|
||||
language: AppConstants.defaultLanguage,
|
||||
currency: AppConstants.defaultCurrency,
|
||||
taxRate: AppConstants.defaultTaxRate,
|
||||
storeName: AppConstants.appName,
|
||||
enableSync: true,
|
||||
);
|
||||
await box.put(StorageConstants.settingsKey, settings);
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateSettings(AppSettingsModel settings) async {
|
||||
await box.put(StorageConstants.settingsKey, settings);
|
||||
}
|
||||
}
|
||||
162
lib/features/settings/data/models/app_settings_model.dart
Normal file
162
lib/features/settings/data/models/app_settings_model.dart
Normal file
@@ -0,0 +1,162 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import '../../domain/entities/app_settings.dart';
|
||||
import '../../../../core/constants/storage_constants.dart';
|
||||
import '../../../../core/constants/app_constants.dart';
|
||||
|
||||
part 'app_settings_model.g.dart';
|
||||
|
||||
@HiveType(typeId: StorageConstants.appSettingsTypeId)
|
||||
class AppSettingsModel extends HiveObject {
|
||||
@HiveField(0)
|
||||
final String themeModeString;
|
||||
|
||||
@HiveField(1)
|
||||
final String language;
|
||||
|
||||
@HiveField(2)
|
||||
final String currency;
|
||||
|
||||
@HiveField(3)
|
||||
final double taxRate;
|
||||
|
||||
@HiveField(4)
|
||||
final String storeName;
|
||||
|
||||
@HiveField(5)
|
||||
final bool enableSync;
|
||||
|
||||
@HiveField(6)
|
||||
final DateTime? lastSyncAt;
|
||||
|
||||
AppSettingsModel({
|
||||
required this.themeModeString,
|
||||
required this.language,
|
||||
required this.currency,
|
||||
required this.taxRate,
|
||||
required this.storeName,
|
||||
required this.enableSync,
|
||||
this.lastSyncAt,
|
||||
});
|
||||
|
||||
/// Convert to domain entity
|
||||
AppSettings toEntity() {
|
||||
ThemeMode themeMode;
|
||||
switch (themeModeString) {
|
||||
case 'light':
|
||||
themeMode = ThemeMode.light;
|
||||
break;
|
||||
case 'dark':
|
||||
themeMode = ThemeMode.dark;
|
||||
break;
|
||||
default:
|
||||
themeMode = ThemeMode.system;
|
||||
}
|
||||
|
||||
return AppSettings(
|
||||
themeMode: themeMode,
|
||||
language: language,
|
||||
currency: currency,
|
||||
taxRate: taxRate,
|
||||
storeName: storeName,
|
||||
enableSync: enableSync,
|
||||
lastSyncAt: lastSyncAt,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create from domain entity
|
||||
factory AppSettingsModel.fromEntity(AppSettings settings) {
|
||||
String themeModeString;
|
||||
switch (settings.themeMode) {
|
||||
case ThemeMode.light:
|
||||
themeModeString = 'light';
|
||||
break;
|
||||
case ThemeMode.dark:
|
||||
themeModeString = 'dark';
|
||||
break;
|
||||
default:
|
||||
themeModeString = 'system';
|
||||
}
|
||||
|
||||
return AppSettingsModel(
|
||||
themeModeString: themeModeString,
|
||||
language: settings.language,
|
||||
currency: settings.currency,
|
||||
taxRate: settings.taxRate,
|
||||
storeName: settings.storeName,
|
||||
enableSync: settings.enableSync,
|
||||
lastSyncAt: settings.lastSyncAt,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create default settings
|
||||
factory AppSettingsModel.defaultSettings() {
|
||||
return AppSettingsModel(
|
||||
themeModeString: 'system',
|
||||
language: AppConstants.defaultLanguage,
|
||||
currency: AppConstants.defaultCurrency,
|
||||
taxRate: AppConstants.defaultTaxRate,
|
||||
storeName: AppConstants.appName,
|
||||
enableSync: true,
|
||||
lastSyncAt: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create from ThemeMode
|
||||
factory AppSettingsModel.fromThemeMode(ThemeMode mode) {
|
||||
String themeModeString;
|
||||
switch (mode) {
|
||||
case ThemeMode.light:
|
||||
themeModeString = 'light';
|
||||
break;
|
||||
case ThemeMode.dark:
|
||||
themeModeString = 'dark';
|
||||
break;
|
||||
default:
|
||||
themeModeString = 'system';
|
||||
}
|
||||
|
||||
return AppSettingsModel(
|
||||
themeModeString: themeModeString,
|
||||
language: AppConstants.defaultLanguage,
|
||||
currency: AppConstants.defaultCurrency,
|
||||
taxRate: AppConstants.defaultTaxRate,
|
||||
storeName: AppConstants.appName,
|
||||
enableSync: true,
|
||||
lastSyncAt: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get ThemeMode from string
|
||||
ThemeMode get themeMode {
|
||||
switch (themeModeString) {
|
||||
case 'light':
|
||||
return ThemeMode.light;
|
||||
case 'dark':
|
||||
return ThemeMode.dark;
|
||||
default:
|
||||
return ThemeMode.system;
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a copy with updated fields
|
||||
AppSettingsModel copyWith({
|
||||
String? themeModeString,
|
||||
String? language,
|
||||
String? currency,
|
||||
double? taxRate,
|
||||
String? storeName,
|
||||
bool? enableSync,
|
||||
DateTime? lastSyncAt,
|
||||
}) {
|
||||
return AppSettingsModel(
|
||||
themeModeString: themeModeString ?? this.themeModeString,
|
||||
language: language ?? this.language,
|
||||
currency: currency ?? this.currency,
|
||||
taxRate: taxRate ?? this.taxRate,
|
||||
storeName: storeName ?? this.storeName,
|
||||
enableSync: enableSync ?? this.enableSync,
|
||||
lastSyncAt: lastSyncAt ?? this.lastSyncAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
59
lib/features/settings/data/models/app_settings_model.g.dart
Normal file
59
lib/features/settings/data/models/app_settings_model.g.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'app_settings_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class AppSettingsModelAdapter extends TypeAdapter<AppSettingsModel> {
|
||||
@override
|
||||
final typeId = 4;
|
||||
|
||||
@override
|
||||
AppSettingsModel read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return AppSettingsModel(
|
||||
themeModeString: fields[0] as String,
|
||||
language: fields[1] as String,
|
||||
currency: fields[2] as String,
|
||||
taxRate: (fields[3] as num).toDouble(),
|
||||
storeName: fields[4] as String,
|
||||
enableSync: fields[5] as bool,
|
||||
lastSyncAt: fields[6] as DateTime?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, AppSettingsModel obj) {
|
||||
writer
|
||||
..writeByte(7)
|
||||
..writeByte(0)
|
||||
..write(obj.themeModeString)
|
||||
..writeByte(1)
|
||||
..write(obj.language)
|
||||
..writeByte(2)
|
||||
..write(obj.currency)
|
||||
..writeByte(3)
|
||||
..write(obj.taxRate)
|
||||
..writeByte(4)
|
||||
..write(obj.storeName)
|
||||
..writeByte(5)
|
||||
..write(obj.enableSync)
|
||||
..writeByte(6)
|
||||
..write(obj.lastSyncAt);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is AppSettingsModelAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../domain/entities/app_settings.dart';
|
||||
import '../../domain/repositories/settings_repository.dart';
|
||||
import '../datasources/settings_local_datasource.dart';
|
||||
import '../models/app_settings_model.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../../../../core/errors/exceptions.dart';
|
||||
|
||||
class SettingsRepositoryImpl implements SettingsRepository {
|
||||
final SettingsLocalDataSource localDataSource;
|
||||
|
||||
SettingsRepositoryImpl({
|
||||
required this.localDataSource,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<Either<Failure, AppSettings>> getSettings() async {
|
||||
try {
|
||||
final settings = await localDataSource.getSettings();
|
||||
return Right(settings.toEntity());
|
||||
} on CacheException catch (e) {
|
||||
return Left(CacheFailure(e.message));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> updateSettings(AppSettings settings) async {
|
||||
try {
|
||||
final model = AppSettingsModel.fromEntity(settings);
|
||||
await localDataSource.updateSettings(model);
|
||||
return const Right(null);
|
||||
} on CacheException catch (e) {
|
||||
return Left(CacheFailure(e.message));
|
||||
}
|
||||
}
|
||||
}
|
||||
68
lib/features/settings/domain/entities/app_settings.dart
Normal file
68
lib/features/settings/domain/entities/app_settings.dart
Normal file
@@ -0,0 +1,68 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/constants/app_constants.dart';
|
||||
|
||||
/// App settings domain entity
|
||||
class AppSettings extends Equatable {
|
||||
final ThemeMode themeMode;
|
||||
final String language;
|
||||
final String currency;
|
||||
final double taxRate;
|
||||
final String storeName;
|
||||
final bool enableSync;
|
||||
final DateTime? lastSyncAt;
|
||||
|
||||
const AppSettings({
|
||||
required this.themeMode,
|
||||
required this.language,
|
||||
required this.currency,
|
||||
required this.taxRate,
|
||||
required this.storeName,
|
||||
required this.enableSync,
|
||||
this.lastSyncAt,
|
||||
});
|
||||
|
||||
AppSettings copyWith({
|
||||
ThemeMode? themeMode,
|
||||
String? language,
|
||||
String? currency,
|
||||
double? taxRate,
|
||||
String? storeName,
|
||||
bool? enableSync,
|
||||
DateTime? lastSyncAt,
|
||||
}) {
|
||||
return AppSettings(
|
||||
themeMode: themeMode ?? this.themeMode,
|
||||
language: language ?? this.language,
|
||||
currency: currency ?? this.currency,
|
||||
taxRate: taxRate ?? this.taxRate,
|
||||
storeName: storeName ?? this.storeName,
|
||||
enableSync: enableSync ?? this.enableSync,
|
||||
lastSyncAt: lastSyncAt ?? this.lastSyncAt,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create default settings
|
||||
factory AppSettings.defaultSettings() {
|
||||
return const AppSettings(
|
||||
themeMode: ThemeMode.system,
|
||||
language: AppConstants.defaultLanguage,
|
||||
currency: AppConstants.defaultCurrency,
|
||||
taxRate: AppConstants.defaultTaxRate,
|
||||
storeName: AppConstants.appName,
|
||||
enableSync: true,
|
||||
lastSyncAt: null,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
themeMode,
|
||||
language,
|
||||
currency,
|
||||
taxRate,
|
||||
storeName,
|
||||
enableSync,
|
||||
lastSyncAt,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../entities/app_settings.dart';
|
||||
|
||||
/// Settings repository interface
|
||||
abstract class SettingsRepository {
|
||||
/// Get app settings
|
||||
Future<Either<Failure, AppSettings>> getSettings();
|
||||
|
||||
/// Update app settings
|
||||
Future<Either<Failure, void>> updateSettings(AppSettings settings);
|
||||
}
|
||||
15
lib/features/settings/domain/usecases/get_settings.dart
Normal file
15
lib/features/settings/domain/usecases/get_settings.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../entities/app_settings.dart';
|
||||
import '../repositories/settings_repository.dart';
|
||||
|
||||
/// Use case to get app settings
|
||||
class GetSettings {
|
||||
final SettingsRepository repository;
|
||||
|
||||
GetSettings(this.repository);
|
||||
|
||||
Future<Either<Failure, AppSettings>> call() async {
|
||||
return await repository.getSettings();
|
||||
}
|
||||
}
|
||||
15
lib/features/settings/domain/usecases/update_settings.dart
Normal file
15
lib/features/settings/domain/usecases/update_settings.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../entities/app_settings.dart';
|
||||
import '../repositories/settings_repository.dart';
|
||||
|
||||
/// Use case to update app settings
|
||||
class UpdateSettings {
|
||||
final SettingsRepository repository;
|
||||
|
||||
UpdateSettings(this.repository);
|
||||
|
||||
Future<Either<Failure, void>> call(AppSettings settings) async {
|
||||
return await repository.updateSettings(settings);
|
||||
}
|
||||
}
|
||||
482
lib/features/settings/presentation/pages/settings_page.dart
Normal file
482
lib/features/settings/presentation/pages/settings_page.dart
Normal file
@@ -0,0 +1,482 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../providers/settings_provider.dart';
|
||||
import '../../../../core/constants/app_constants.dart';
|
||||
|
||||
/// Settings page
|
||||
class SettingsPage extends ConsumerWidget {
|
||||
const SettingsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final settingsAsync = ref.watch(settingsProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Settings'),
|
||||
),
|
||||
body: settingsAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) => Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('Error: $error'),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () => ref.invalidate(settingsProvider),
|
||||
child: const Text('Retry'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
data: (settings) {
|
||||
return ListView(
|
||||
children: [
|
||||
// Appearance Section
|
||||
_buildSectionHeader(context, 'Appearance'),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.palette_outlined),
|
||||
title: const Text('Theme'),
|
||||
subtitle: Text(_getThemeModeName(settings.themeMode)),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
_showThemeDialog(context, ref, settings.themeMode);
|
||||
},
|
||||
),
|
||||
|
||||
const Divider(),
|
||||
|
||||
// Localization Section
|
||||
_buildSectionHeader(context, 'Localization'),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.language),
|
||||
title: const Text('Language'),
|
||||
subtitle: Text(_getLanguageName(settings.language)),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
_showLanguageDialog(context, ref, settings.language);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.attach_money),
|
||||
title: const Text('Currency'),
|
||||
subtitle: Text(settings.currency),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
_showCurrencyDialog(context, ref, settings.currency);
|
||||
},
|
||||
),
|
||||
|
||||
const Divider(),
|
||||
|
||||
// Business Settings Section
|
||||
_buildSectionHeader(context, 'Business Settings'),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.store),
|
||||
title: const Text('Store Name'),
|
||||
subtitle: Text(settings.storeName),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
_showStoreNameDialog(context, ref, settings.storeName);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.percent),
|
||||
title: const Text('Tax Rate'),
|
||||
subtitle: Text('${(settings.taxRate * 100).toStringAsFixed(1)}%'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
_showTaxRateDialog(context, ref, settings.taxRate);
|
||||
},
|
||||
),
|
||||
|
||||
const Divider(),
|
||||
|
||||
// Data Management Section
|
||||
_buildSectionHeader(context, 'Data Management'),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.sync),
|
||||
title: const Text('Sync Data'),
|
||||
subtitle: settings.lastSyncAt != null
|
||||
? Text('Last synced: ${_formatDateTime(settings.lastSyncAt!)}')
|
||||
: const Text('Never synced'),
|
||||
trailing: const Icon(Icons.cloud_upload),
|
||||
onTap: () => _performSync(context, ref),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_sweep),
|
||||
title: const Text('Clear Cache'),
|
||||
subtitle: const Text('Remove cached images and data'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _showClearCacheDialog(context),
|
||||
),
|
||||
|
||||
const Divider(),
|
||||
|
||||
// About Section
|
||||
_buildSectionHeader(context, 'About'),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
title: const Text('App Version'),
|
||||
subtitle: Text(AppConstants.appVersion),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.business),
|
||||
title: const Text('About ${AppConstants.appName}'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _showAboutDialog(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader(BuildContext context, String title) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getThemeModeName(ThemeMode mode) {
|
||||
switch (mode) {
|
||||
case ThemeMode.light:
|
||||
return 'Light';
|
||||
case ThemeMode.dark:
|
||||
return 'Dark';
|
||||
case ThemeMode.system:
|
||||
return 'System';
|
||||
}
|
||||
}
|
||||
|
||||
String _getLanguageName(String code) {
|
||||
switch (code) {
|
||||
case 'en':
|
||||
return 'English';
|
||||
case 'es':
|
||||
return 'Spanish';
|
||||
case 'fr':
|
||||
return 'French';
|
||||
default:
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
String _formatDateTime(DateTime dateTime) {
|
||||
return '${dateTime.day}/${dateTime.month}/${dateTime.year} ${dateTime.hour}:${dateTime.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
void _showThemeDialog(BuildContext context, WidgetRef ref, ThemeMode currentMode) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Select Theme'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
RadioListTile<ThemeMode>(
|
||||
title: const Text('Light'),
|
||||
value: ThemeMode.light,
|
||||
groupValue: currentMode,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
ref.read(settingsProvider.notifier).updateTheme(value);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
RadioListTile<ThemeMode>(
|
||||
title: const Text('Dark'),
|
||||
value: ThemeMode.dark,
|
||||
groupValue: currentMode,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
ref.read(settingsProvider.notifier).updateTheme(value);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
RadioListTile<ThemeMode>(
|
||||
title: const Text('System'),
|
||||
value: ThemeMode.system,
|
||||
groupValue: currentMode,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
ref.read(settingsProvider.notifier).updateTheme(value);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showLanguageDialog(BuildContext context, WidgetRef ref, String currentLanguage) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Select Language'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
RadioListTile<String>(
|
||||
title: const Text('English'),
|
||||
value: 'en',
|
||||
groupValue: currentLanguage,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
ref.read(settingsProvider.notifier).updateLanguage(value);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
RadioListTile<String>(
|
||||
title: const Text('Spanish'),
|
||||
value: 'es',
|
||||
groupValue: currentLanguage,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
ref.read(settingsProvider.notifier).updateLanguage(value);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
RadioListTile<String>(
|
||||
title: const Text('French'),
|
||||
value: 'fr',
|
||||
groupValue: currentLanguage,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
ref.read(settingsProvider.notifier).updateLanguage(value);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showCurrencyDialog(BuildContext context, WidgetRef ref, String currentCurrency) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Select Currency'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
RadioListTile<String>(
|
||||
title: const Text('USD - US Dollar'),
|
||||
value: 'USD',
|
||||
groupValue: currentCurrency,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
// TODO: Implement currency update
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
RadioListTile<String>(
|
||||
title: const Text('EUR - Euro'),
|
||||
value: 'EUR',
|
||||
groupValue: currentCurrency,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
// TODO: Implement currency update
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
RadioListTile<String>(
|
||||
title: const Text('GBP - British Pound'),
|
||||
value: 'GBP',
|
||||
groupValue: currentCurrency,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
// TODO: Implement currency update
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showStoreNameDialog(BuildContext context, WidgetRef ref, String currentName) {
|
||||
final controller = TextEditingController(text: currentName);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Store Name'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Store Name',
|
||||
hintText: 'Enter store name',
|
||||
),
|
||||
autofocus: true,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
// TODO: Implement store name update
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Store name updated')),
|
||||
);
|
||||
},
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showTaxRateDialog(BuildContext context, WidgetRef ref, double currentRate) {
|
||||
final controller = TextEditingController(
|
||||
text: (currentRate * 100).toStringAsFixed(1),
|
||||
);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Tax Rate'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Tax Rate (%)',
|
||||
hintText: 'Enter tax rate',
|
||||
suffixText: '%',
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
autofocus: true,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
// TODO: Implement tax rate update
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Tax rate updated')),
|
||||
);
|
||||
},
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _performSync(BuildContext context, WidgetRef ref) async {
|
||||
// Show loading dialog
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => const Center(
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('Syncing data...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Perform sync
|
||||
await Future.delayed(const Duration(seconds: 2)); // Simulated delay
|
||||
|
||||
// Close loading dialog
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Data synced successfully')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _showClearCacheDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Clear Cache'),
|
||||
content: const Text(
|
||||
'This will remove all cached images and data. The app may need to reload content from the server.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Cache cleared')),
|
||||
);
|
||||
},
|
||||
child: const Text('Clear'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAboutDialog(BuildContext context) {
|
||||
showAboutDialog(
|
||||
context: context,
|
||||
applicationName: AppConstants.appName,
|
||||
applicationVersion: AppConstants.appVersion,
|
||||
applicationIcon: const Icon(Icons.store, size: 48),
|
||||
children: [
|
||||
const Text(
|
||||
'A modern Point of Sale application built with Flutter.',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Features:\n'
|
||||
'• Product management\n'
|
||||
'• Category organization\n'
|
||||
'• Shopping cart\n'
|
||||
'• Transaction processing\n'
|
||||
'• Offline-first architecture',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'settings_provider.dart';
|
||||
|
||||
part 'language_provider.g.dart';
|
||||
|
||||
/// Language/locale provider
|
||||
/// Extracts language from settings for easy access
|
||||
@riverpod
|
||||
String appLanguage(Ref ref) {
|
||||
final settingsAsync = ref.watch(settingsProvider);
|
||||
return settingsAsync.when(
|
||||
data: (settings) => settings.language,
|
||||
loading: () => 'en',
|
||||
error: (_, __) => 'en',
|
||||
);
|
||||
}
|
||||
|
||||
/// Supported languages provider
|
||||
@riverpod
|
||||
List<LanguageOption> supportedLanguages(Ref ref) {
|
||||
return const [
|
||||
LanguageOption(code: 'en', name: 'English', nativeName: 'English'),
|
||||
LanguageOption(code: 'es', name: 'Spanish', nativeName: 'Español'),
|
||||
LanguageOption(code: 'fr', name: 'French', nativeName: 'Français'),
|
||||
LanguageOption(code: 'de', name: 'German', nativeName: 'Deutsch'),
|
||||
LanguageOption(code: 'it', name: 'Italian', nativeName: 'Italiano'),
|
||||
LanguageOption(code: 'pt', name: 'Portuguese', nativeName: 'Português'),
|
||||
LanguageOption(code: 'zh', name: 'Chinese', nativeName: '中文'),
|
||||
LanguageOption(code: 'ja', name: 'Japanese', nativeName: '日本語'),
|
||||
LanguageOption(code: 'ko', name: 'Korean', nativeName: '한국어'),
|
||||
LanguageOption(code: 'ar', name: 'Arabic', nativeName: 'العربية'),
|
||||
];
|
||||
}
|
||||
|
||||
/// Language option model
|
||||
class LanguageOption {
|
||||
final String code;
|
||||
final String name;
|
||||
final String nativeName;
|
||||
|
||||
const LanguageOption({
|
||||
required this.code,
|
||||
required this.name,
|
||||
required this.nativeName,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'language_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Language/locale provider
|
||||
/// Extracts language from settings for easy access
|
||||
|
||||
@ProviderFor(appLanguage)
|
||||
const appLanguageProvider = AppLanguageProvider._();
|
||||
|
||||
/// Language/locale provider
|
||||
/// Extracts language from settings for easy access
|
||||
|
||||
final class AppLanguageProvider
|
||||
extends $FunctionalProvider<String, String, String>
|
||||
with $Provider<String> {
|
||||
/// Language/locale provider
|
||||
/// Extracts language from settings for easy access
|
||||
const AppLanguageProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'appLanguageProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$appLanguageHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<String> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
String create(Ref ref) {
|
||||
return appLanguage(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(String value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<String>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$appLanguageHash() => r'c5bfde42820d2fa742b4c875b91a0081ae235d41';
|
||||
|
||||
/// Supported languages provider
|
||||
|
||||
@ProviderFor(supportedLanguages)
|
||||
const supportedLanguagesProvider = SupportedLanguagesProvider._();
|
||||
|
||||
/// Supported languages provider
|
||||
|
||||
final class SupportedLanguagesProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
List<LanguageOption>,
|
||||
List<LanguageOption>,
|
||||
List<LanguageOption>
|
||||
>
|
||||
with $Provider<List<LanguageOption>> {
|
||||
/// Supported languages provider
|
||||
const SupportedLanguagesProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'supportedLanguagesProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$supportedLanguagesHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<List<LanguageOption>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
List<LanguageOption> create(Ref ref) {
|
||||
return supportedLanguages(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(List<LanguageOption> value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<List<LanguageOption>>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$supportedLanguagesHash() =>
|
||||
r'c4b8224c1504112ce36de33ca7d3cf34d785a120';
|
||||
@@ -0,0 +1,5 @@
|
||||
/// Export all settings providers
|
||||
export 'settings_datasource_provider.dart';
|
||||
export 'settings_provider.dart';
|
||||
export 'theme_provider.dart';
|
||||
export 'language_provider.dart';
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import '../../data/datasources/settings_local_datasource.dart';
|
||||
import '../../../../core/database/hive_database.dart';
|
||||
import '../../data/models/app_settings_model.dart';
|
||||
|
||||
part 'settings_datasource_provider.g.dart';
|
||||
|
||||
/// Provider for settings local data source
|
||||
/// This is kept alive as it's a dependency injection provider
|
||||
@Riverpod(keepAlive: true)
|
||||
SettingsLocalDataSource settingsLocalDataSource(Ref ref) {
|
||||
final box = HiveDatabase.instance.getBox<AppSettingsModel>('settings');
|
||||
return SettingsLocalDataSourceImpl(box);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'settings_datasource_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Provider for settings local data source
|
||||
/// This is kept alive as it's a dependency injection provider
|
||||
|
||||
@ProviderFor(settingsLocalDataSource)
|
||||
const settingsLocalDataSourceProvider = SettingsLocalDataSourceProvider._();
|
||||
|
||||
/// Provider for settings local data source
|
||||
/// This is kept alive as it's a dependency injection provider
|
||||
|
||||
final class SettingsLocalDataSourceProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
SettingsLocalDataSource,
|
||||
SettingsLocalDataSource,
|
||||
SettingsLocalDataSource
|
||||
>
|
||||
with $Provider<SettingsLocalDataSource> {
|
||||
/// Provider for settings local data source
|
||||
/// This is kept alive as it's a dependency injection provider
|
||||
const SettingsLocalDataSourceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'settingsLocalDataSourceProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$settingsLocalDataSourceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<SettingsLocalDataSource> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
SettingsLocalDataSource create(Ref ref) {
|
||||
return settingsLocalDataSource(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(SettingsLocalDataSource value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<SettingsLocalDataSource>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$settingsLocalDataSourceHash() =>
|
||||
r'fe7c05c34da176079f5bb95cc3a410d5fb5f3f68';
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import '../../domain/entities/app_settings.dart';
|
||||
import '../../../../core/constants/app_constants.dart';
|
||||
|
||||
part 'settings_provider.g.dart';
|
||||
|
||||
/// Provider for app settings
|
||||
@riverpod
|
||||
class Settings extends _$Settings {
|
||||
@override
|
||||
Future<AppSettings> build() async {
|
||||
// TODO: Implement with repository
|
||||
// Return default settings for now
|
||||
return const AppSettings(
|
||||
themeMode: ThemeMode.system,
|
||||
language: AppConstants.defaultLanguage,
|
||||
currency: AppConstants.defaultCurrency,
|
||||
taxRate: AppConstants.defaultTaxRate,
|
||||
storeName: AppConstants.appName,
|
||||
enableSync: true,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateTheme(ThemeMode mode) async {
|
||||
final current = state.value;
|
||||
if (current != null) {
|
||||
final updated = current.copyWith(themeMode: mode);
|
||||
state = AsyncValue.data(updated);
|
||||
// TODO: Persist to repository
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateLanguage(String language) async {
|
||||
final current = state.value;
|
||||
if (current != null) {
|
||||
final updated = current.copyWith(language: language);
|
||||
state = AsyncValue.data(updated);
|
||||
// TODO: Persist to repository
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateLastSyncTime() async {
|
||||
final current = state.value;
|
||||
if (current != null) {
|
||||
final updated = current.copyWith(lastSyncAt: DateTime.now());
|
||||
state = AsyncValue.data(updated);
|
||||
// TODO: Persist to repository
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'settings_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Provider for app settings
|
||||
|
||||
@ProviderFor(Settings)
|
||||
const settingsProvider = SettingsProvider._();
|
||||
|
||||
/// Provider for app settings
|
||||
final class SettingsProvider
|
||||
extends $AsyncNotifierProvider<Settings, AppSettings> {
|
||||
/// Provider for app settings
|
||||
const SettingsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'settingsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$settingsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
Settings create() => Settings();
|
||||
}
|
||||
|
||||
String _$settingsHash() => r'17065d5a6818ea746a031f33ff7f4fb9ab111075';
|
||||
|
||||
/// Provider for app settings
|
||||
|
||||
abstract class _$Settings extends $AsyncNotifier<AppSettings> {
|
||||
FutureOr<AppSettings> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<AsyncValue<AppSettings>, AppSettings>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<AppSettings>, AppSettings>,
|
||||
AsyncValue<AppSettings>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'settings_provider.dart';
|
||||
|
||||
part 'theme_provider.g.dart';
|
||||
|
||||
/// Theme mode provider from theme_provider
|
||||
/// Extracts theme mode from settings for easy access
|
||||
@riverpod
|
||||
ThemeMode themeModeFromTheme(Ref ref) {
|
||||
final settingsAsync = ref.watch(settingsProvider);
|
||||
return settingsAsync.when(
|
||||
data: (settings) => settings.themeMode,
|
||||
loading: () => ThemeMode.system,
|
||||
error: (_, __) => ThemeMode.system,
|
||||
);
|
||||
}
|
||||
|
||||
/// Provider to check if dark mode is active
|
||||
@riverpod
|
||||
bool isDarkMode(Ref ref) {
|
||||
final mode = ref.watch(themeModeFromThemeProvider);
|
||||
return mode == ThemeMode.dark;
|
||||
}
|
||||
|
||||
/// Provider to check if light mode is active
|
||||
@riverpod
|
||||
bool isLightMode(Ref ref) {
|
||||
final mode = ref.watch(themeModeFromThemeProvider);
|
||||
return mode == ThemeMode.light;
|
||||
}
|
||||
|
||||
/// Provider to check if system theme is active
|
||||
@riverpod
|
||||
bool isSystemTheme(Ref ref) {
|
||||
final mode = ref.watch(themeModeFromThemeProvider);
|
||||
return mode == ThemeMode.system;
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'theme_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Theme mode provider from theme_provider
|
||||
/// Extracts theme mode from settings for easy access
|
||||
|
||||
@ProviderFor(themeModeFromTheme)
|
||||
const themeModeFromThemeProvider = ThemeModeFromThemeProvider._();
|
||||
|
||||
/// Theme mode provider from theme_provider
|
||||
/// Extracts theme mode from settings for easy access
|
||||
|
||||
final class ThemeModeFromThemeProvider
|
||||
extends $FunctionalProvider<ThemeMode, ThemeMode, ThemeMode>
|
||||
with $Provider<ThemeMode> {
|
||||
/// Theme mode provider from theme_provider
|
||||
/// Extracts theme mode from settings for easy access
|
||||
const ThemeModeFromThemeProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'themeModeFromThemeProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$themeModeFromThemeHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<ThemeMode> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
ThemeMode create(Ref ref) {
|
||||
return themeModeFromTheme(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(ThemeMode value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<ThemeMode>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$themeModeFromThemeHash() =>
|
||||
r'a906c8a301f2ac5e4b83009b239eb3a6f049a1b1';
|
||||
|
||||
/// Provider to check if dark mode is active
|
||||
|
||||
@ProviderFor(isDarkMode)
|
||||
const isDarkModeProvider = IsDarkModeProvider._();
|
||||
|
||||
/// Provider to check if dark mode is active
|
||||
|
||||
final class IsDarkModeProvider extends $FunctionalProvider<bool, bool, bool>
|
||||
with $Provider<bool> {
|
||||
/// Provider to check if dark mode is active
|
||||
const IsDarkModeProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'isDarkModeProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$isDarkModeHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<bool> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
bool create(Ref ref) {
|
||||
return isDarkMode(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(bool value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<bool>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$isDarkModeHash() => r'f8c2497b2bae2519f51da2543e4fc7e99a4f5b8c';
|
||||
|
||||
/// Provider to check if light mode is active
|
||||
|
||||
@ProviderFor(isLightMode)
|
||||
const isLightModeProvider = IsLightModeProvider._();
|
||||
|
||||
/// Provider to check if light mode is active
|
||||
|
||||
final class IsLightModeProvider extends $FunctionalProvider<bool, bool, bool>
|
||||
with $Provider<bool> {
|
||||
/// Provider to check if light mode is active
|
||||
const IsLightModeProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'isLightModeProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$isLightModeHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<bool> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
bool create(Ref ref) {
|
||||
return isLightMode(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(bool value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<bool>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$isLightModeHash() => r'0aac9dd8e1cb428913b5d463635dcc7b9315f031';
|
||||
|
||||
/// Provider to check if system theme is active
|
||||
|
||||
@ProviderFor(isSystemTheme)
|
||||
const isSystemThemeProvider = IsSystemThemeProvider._();
|
||||
|
||||
/// Provider to check if system theme is active
|
||||
|
||||
final class IsSystemThemeProvider extends $FunctionalProvider<bool, bool, bool>
|
||||
with $Provider<bool> {
|
||||
/// Provider to check if system theme is active
|
||||
const IsSystemThemeProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'isSystemThemeProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$isSystemThemeHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<bool> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
bool create(Ref ref) {
|
||||
return isSystemTheme(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(bool value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<bool>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$isSystemThemeHash() => r'80e8bef39cde0b6f1b3e074483ea30d5a64aeade';
|
||||
Reference in New Issue
Block a user