fix
This commit is contained in:
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"files.watcherExclude": {
|
||||||
|
"**/.git/objects/**": true,
|
||||||
|
"**/.git/subtree-cache/**": true,
|
||||||
|
"**/.hg/store/**": true,
|
||||||
|
"**/.dart_tool": true,
|
||||||
|
"**/.git/**": true,
|
||||||
|
"**/node_modules/**": true,
|
||||||
|
"**/.vscode/**": true
|
||||||
|
}
|
||||||
|
}
|
||||||
55
lib/app.dart
55
lib/app.dart
@@ -1,15 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'core/router/app_router.dart';
|
||||||
import 'core/theme/app_theme.dart';
|
import 'core/theme/app_theme.dart';
|
||||||
import 'features/auth/presentation/presentation.dart';
|
import 'features/auth/presentation/providers/auth_provider.dart';
|
||||||
import 'features/home/presentation/pages/home_page.dart';
|
|
||||||
import 'features/products/presentation/pages/products_page.dart';
|
|
||||||
import 'features/categories/presentation/pages/categories_page.dart';
|
|
||||||
import 'features/settings/presentation/pages/settings_page.dart';
|
|
||||||
import 'features/settings/presentation/providers/theme_provider.dart';
|
import 'features/settings/presentation/providers/theme_provider.dart';
|
||||||
import 'shared/widgets/app_bottom_nav.dart';
|
|
||||||
|
|
||||||
/// Root application widget with authentication wrapper
|
/// Root application widget with go_router integration
|
||||||
class RetailApp extends ConsumerStatefulWidget {
|
class RetailApp extends ConsumerStatefulWidget {
|
||||||
const RetailApp({super.key});
|
const RetailApp({super.key});
|
||||||
|
|
||||||
@@ -32,54 +28,15 @@ class _RetailAppState extends ConsumerState<RetailApp> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final themeMode = ref.watch(themeModeFromThemeProvider);
|
final themeMode = ref.watch(themeModeFromThemeProvider);
|
||||||
|
final router = ref.watch(routerProvider);
|
||||||
|
|
||||||
return MaterialApp(
|
return MaterialApp.router(
|
||||||
title: 'Retail POS',
|
title: 'Retail POS',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: AppTheme.lightTheme,
|
theme: AppTheme.lightTheme,
|
||||||
darkTheme: AppTheme.darkTheme,
|
darkTheme: AppTheme.darkTheme,
|
||||||
themeMode: themeMode,
|
themeMode: themeMode,
|
||||||
// Wrap the home with AuthWrapper to require login
|
routerConfig: router,
|
||||||
home: const AuthWrapper(
|
|
||||||
child: MainScreen(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Main screen with bottom navigation (only accessible after login)
|
|
||||||
class MainScreen extends ConsumerStatefulWidget {
|
|
||||||
const MainScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
ConsumerState<MainScreen> createState() => _MainScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MainScreenState extends ConsumerState<MainScreen> {
|
|
||||||
int _currentIndex = 0;
|
|
||||||
|
|
||||||
final List<Widget> _pages = const [
|
|
||||||
HomePage(),
|
|
||||||
ProductsPage(),
|
|
||||||
CategoriesPage(),
|
|
||||||
SettingsPage(),
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
body: IndexedStack(
|
|
||||||
index: _currentIndex,
|
|
||||||
children: _pages,
|
|
||||||
),
|
|
||||||
bottomNavigationBar: AppBottomNav(
|
|
||||||
currentIndex: _currentIndex,
|
|
||||||
onTap: (index) {
|
|
||||||
setState(() {
|
|
||||||
_currentIndex = index;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
156
lib/core/router/app_router.dart
Normal file
156
lib/core/router/app_router.dart
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import '../../features/auth/presentation/pages/login_page.dart';
|
||||||
|
import '../../features/auth/presentation/pages/register_page.dart';
|
||||||
|
import '../../features/auth/presentation/providers/auth_provider.dart';
|
||||||
|
import '../../features/auth/presentation/widgets/splash_screen.dart';
|
||||||
|
import '../../features/categories/presentation/pages/categories_page.dart';
|
||||||
|
import '../../features/categories/presentation/pages/category_detail_page.dart';
|
||||||
|
import '../../features/home/presentation/pages/home_page.dart';
|
||||||
|
import '../../features/products/presentation/pages/batch_update_page.dart';
|
||||||
|
import '../../features/products/presentation/pages/product_detail_page.dart';
|
||||||
|
import '../../features/products/presentation/pages/products_page.dart';
|
||||||
|
import '../../features/settings/presentation/pages/settings_page.dart';
|
||||||
|
import '../../shared/widgets/app_bottom_nav_shell.dart';
|
||||||
|
|
||||||
|
part 'app_router.g.dart';
|
||||||
|
|
||||||
|
/// Router configuration provider
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
GoRouter router(Ref ref) {
|
||||||
|
final authState = ref.watch(authProvider);
|
||||||
|
|
||||||
|
return GoRouter(
|
||||||
|
initialLocation: '/',
|
||||||
|
debugLogDiagnostics: true,
|
||||||
|
redirect: (context, state) {
|
||||||
|
final isAuthenticated = authState.isAuthenticated;
|
||||||
|
final isLoading = authState.isLoading && authState.user == null;
|
||||||
|
final isGoingToAuth = state.matchedLocation == '/login' ||
|
||||||
|
state.matchedLocation == '/register';
|
||||||
|
|
||||||
|
// Show splash screen while loading
|
||||||
|
if (isLoading) {
|
||||||
|
return '/splash';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to login if not authenticated and not already going to auth pages
|
||||||
|
if (!isAuthenticated && !isGoingToAuth && state.matchedLocation != '/splash') {
|
||||||
|
return '/login';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to home if authenticated and going to auth pages
|
||||||
|
if (isAuthenticated && isGoingToAuth) {
|
||||||
|
return '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
routes: [
|
||||||
|
// Splash screen
|
||||||
|
GoRoute(
|
||||||
|
path: '/splash',
|
||||||
|
name: 'splash',
|
||||||
|
builder: (context, state) => const SplashScreen(),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Auth routes
|
||||||
|
GoRoute(
|
||||||
|
path: '/login',
|
||||||
|
name: 'login',
|
||||||
|
builder: (context, state) => const LoginPage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/register',
|
||||||
|
name: 'register',
|
||||||
|
builder: (context, state) => const RegisterPage(),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Main shell with bottom navigation
|
||||||
|
ShellRoute(
|
||||||
|
builder: (context, state, child) {
|
||||||
|
return AppBottomNavShell(child: child);
|
||||||
|
},
|
||||||
|
routes: [
|
||||||
|
// Home tab
|
||||||
|
GoRoute(
|
||||||
|
path: '/',
|
||||||
|
name: 'home',
|
||||||
|
pageBuilder: (context, state) => NoTransitionPage(
|
||||||
|
key: state.pageKey,
|
||||||
|
child: const HomePage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Products tab
|
||||||
|
GoRoute(
|
||||||
|
path: '/products',
|
||||||
|
name: 'products',
|
||||||
|
pageBuilder: (context, state) => NoTransitionPage(
|
||||||
|
key: state.pageKey,
|
||||||
|
child: const ProductsPage(),
|
||||||
|
),
|
||||||
|
routes: [
|
||||||
|
// Product detail
|
||||||
|
GoRoute(
|
||||||
|
path: ':productId',
|
||||||
|
name: 'product-detail',
|
||||||
|
builder: (context, state) {
|
||||||
|
final productId = state.pathParameters['productId']!;
|
||||||
|
return ProductDetailPage(productId: productId);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Batch update
|
||||||
|
GoRoute(
|
||||||
|
path: 'batch-update',
|
||||||
|
name: 'batch-update',
|
||||||
|
builder: (context, state) {
|
||||||
|
// Get selected products from extra parameter
|
||||||
|
final selectedProducts = state.extra as List<dynamic>?;
|
||||||
|
if (selectedProducts == null) {
|
||||||
|
// If no products provided, return to products page
|
||||||
|
return const ProductsPage();
|
||||||
|
}
|
||||||
|
return BatchUpdatePage(
|
||||||
|
selectedProducts: selectedProducts.cast(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Categories tab
|
||||||
|
GoRoute(
|
||||||
|
path: '/categories',
|
||||||
|
name: 'categories',
|
||||||
|
pageBuilder: (context, state) => NoTransitionPage(
|
||||||
|
key: state.pageKey,
|
||||||
|
child: const CategoriesPage(),
|
||||||
|
),
|
||||||
|
routes: [
|
||||||
|
// Category detail
|
||||||
|
GoRoute(
|
||||||
|
path: ':categoryId',
|
||||||
|
name: 'category-detail',
|
||||||
|
builder: (context, state) {
|
||||||
|
final categoryId = state.pathParameters['categoryId']!;
|
||||||
|
return CategoryDetailPage(categoryId: categoryId);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Settings tab
|
||||||
|
GoRoute(
|
||||||
|
path: '/settings',
|
||||||
|
name: 'settings',
|
||||||
|
pageBuilder: (context, state) => NoTransitionPage(
|
||||||
|
key: state.pageKey,
|
||||||
|
child: const SettingsPage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
55
lib/core/router/app_router.g.dart
Normal file
55
lib/core/router/app_router.g.dart
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'app_router.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
/// Router configuration provider
|
||||||
|
|
||||||
|
@ProviderFor(router)
|
||||||
|
const routerProvider = RouterProvider._();
|
||||||
|
|
||||||
|
/// Router configuration provider
|
||||||
|
|
||||||
|
final class RouterProvider
|
||||||
|
extends $FunctionalProvider<GoRouter, GoRouter, GoRouter>
|
||||||
|
with $Provider<GoRouter> {
|
||||||
|
/// Router configuration provider
|
||||||
|
const RouterProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'routerProvider',
|
||||||
|
isAutoDispose: false,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$routerHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$ProviderElement<GoRouter> $createElement($ProviderPointer pointer) =>
|
||||||
|
$ProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
GoRouter create(Ref ref) {
|
||||||
|
return router(ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(GoRouter value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<GoRouter>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$routerHash() => r'3c7108371f8529a70e1e479728e8da132246bab4';
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import '../providers/auth_provider.dart';
|
import '../providers/auth_provider.dart';
|
||||||
import '../widgets/widgets.dart';
|
import '../widgets/widgets.dart';
|
||||||
import '../utils/validators.dart';
|
import '../utils/validators.dart';
|
||||||
import 'register_page.dart';
|
|
||||||
|
|
||||||
/// Login page with email and password authentication
|
/// Login page with email and password authentication
|
||||||
class LoginPage extends ConsumerStatefulWidget {
|
class LoginPage extends ConsumerStatefulWidget {
|
||||||
@@ -66,11 +66,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _navigateToRegister() {
|
void _navigateToRegister() {
|
||||||
Navigator.of(context).push(
|
context.push('/register');
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => const RegisterPage(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleForgotPassword() {
|
void _handleForgotPassword() {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import '../providers/auth_provider.dart';
|
import '../providers/auth_provider.dart';
|
||||||
import '../widgets/widgets.dart';
|
import '../widgets/widgets.dart';
|
||||||
import '../utils/validators.dart';
|
import '../utils/validators.dart';
|
||||||
@@ -90,7 +91,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _navigateBackToLogin() {
|
void _navigateBackToLogin() {
|
||||||
Navigator.of(context).pop();
|
context.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ final class AuthProvider extends $NotifierProvider<Auth, AuthState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$authHash() => r'73c9e7b70799eba2904eb6fc65454332d4146a33';
|
String _$authHash() => r'24ad5a5313febf1a3ac2550adaf19f34098a8f7c';
|
||||||
|
|
||||||
/// Auth state notifier provider
|
/// Auth state notifier provider
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../../domain/entities/category.dart';
|
import '../../domain/entities/category.dart';
|
||||||
|
import '../providers/categories_provider.dart';
|
||||||
import '../../../products/presentation/providers/products_provider.dart';
|
import '../../../products/presentation/providers/products_provider.dart';
|
||||||
import '../../../products/presentation/widgets/product_card.dart';
|
import '../../../products/presentation/widgets/product_card.dart';
|
||||||
import '../../../products/presentation/widgets/product_list_item.dart';
|
import '../../../products/presentation/widgets/product_list_item.dart';
|
||||||
@@ -10,11 +11,11 @@ enum ViewMode { grid, list }
|
|||||||
|
|
||||||
/// Category detail page showing products in the category
|
/// Category detail page showing products in the category
|
||||||
class CategoryDetailPage extends ConsumerStatefulWidget {
|
class CategoryDetailPage extends ConsumerStatefulWidget {
|
||||||
final Category category;
|
final String categoryId;
|
||||||
|
|
||||||
const CategoryDetailPage({
|
const CategoryDetailPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.category,
|
required this.categoryId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -26,80 +27,178 @@ class _CategoryDetailPageState extends ConsumerState<CategoryDetailPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final categoriesAsync = ref.watch(categoriesProvider);
|
||||||
final productsAsync = ref.watch(productsProvider);
|
final productsAsync = ref.watch(productsProvider);
|
||||||
|
|
||||||
return Scaffold(
|
return categoriesAsync.when(
|
||||||
appBar: AppBar(
|
data: (categories) {
|
||||||
title: Text(widget.category.name),
|
// Find the category by ID
|
||||||
actions: [
|
Category? category;
|
||||||
// View mode toggle
|
try {
|
||||||
IconButton(
|
category = categories.firstWhere((c) => c.id == widget.categoryId);
|
||||||
icon: Icon(
|
} catch (e) {
|
||||||
_viewMode == ViewMode.grid
|
// Category not found
|
||||||
? Icons.view_list_rounded
|
category = null;
|
||||||
: Icons.grid_view_rounded,
|
}
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
_viewMode =
|
|
||||||
_viewMode == ViewMode.grid ? ViewMode.list : ViewMode.grid;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
tooltip: _viewMode == ViewMode.grid
|
|
||||||
? 'Switch to list view'
|
|
||||||
: 'Switch to grid view',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: productsAsync.when(
|
|
||||||
data: (products) {
|
|
||||||
// Filter products by category
|
|
||||||
final categoryProducts = products
|
|
||||||
.where((product) => product.categoryId == widget.category.id)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (categoryProducts.isEmpty) {
|
// Handle category not found
|
||||||
return Center(
|
if (category == null) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Category Not Found'),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Icons.inventory_2_outlined,
|
Icons.error_outline,
|
||||||
size: 64,
|
size: 64,
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.error,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'No products in this category',
|
'Category not found',
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Products will appear here once added',
|
'The category you are looking for does not exist',
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
label: const Text('Go Back'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}
|
|
||||||
|
|
||||||
return RefreshIndicator(
|
|
||||||
onRefresh: () async {
|
|
||||||
await ref.read(productsProvider.notifier).syncProducts();
|
|
||||||
},
|
|
||||||
child: _viewMode == ViewMode.grid
|
|
||||||
? _buildGridView(categoryProducts)
|
|
||||||
: _buildListView(categoryProducts),
|
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
loading: () => const Center(
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(category.name),
|
||||||
|
actions: [
|
||||||
|
// View mode toggle
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_viewMode == ViewMode.grid
|
||||||
|
? Icons.view_list_rounded
|
||||||
|
: Icons.grid_view_rounded,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_viewMode =
|
||||||
|
_viewMode == ViewMode.grid ? ViewMode.list : ViewMode.grid;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip: _viewMode == ViewMode.grid
|
||||||
|
? 'Switch to list view'
|
||||||
|
: 'Switch to grid view',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: productsAsync.when(
|
||||||
|
data: (products) {
|
||||||
|
// Filter products by category
|
||||||
|
final categoryProducts = products
|
||||||
|
.where((product) => product.categoryId == category!.id)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (categoryProducts.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.inventory_2_outlined,
|
||||||
|
size: 64,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'No products in this category',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Products will appear here once added',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: () async {
|
||||||
|
await ref.read(productsProvider.notifier).syncProducts();
|
||||||
|
},
|
||||||
|
child: _viewMode == ViewMode.grid
|
||||||
|
? _buildGridView(categoryProducts)
|
||||||
|
: _buildListView(categoryProducts),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
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 products',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
error.toString(),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
ref.invalidate(productsProvider);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
label: const Text('Retry'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading: () => Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Loading...'),
|
||||||
|
),
|
||||||
|
body: const Center(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
error: (error, stack) => Center(
|
),
|
||||||
|
error: (error, stack) => Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Error'),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@@ -110,7 +209,7 @@ class _CategoryDetailPageState extends ConsumerState<CategoryDetailPage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'Error loading products',
|
'Error loading category',
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -122,7 +221,7 @@ class _CategoryDetailPageState extends ConsumerState<CategoryDetailPage> {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.invalidate(productsProvider);
|
ref.invalidate(categoriesProvider);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
label: const Text('Retry'),
|
label: const Text('Retry'),
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import '../../../../core/providers/providers.dart';
|
|||||||
part 'categories_provider.g.dart';
|
part 'categories_provider.g.dart';
|
||||||
|
|
||||||
/// Provider for categories list with online-first approach
|
/// Provider for categories list with online-first approach
|
||||||
@riverpod
|
/// keepAlive ensures data persists when switching tabs
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
class Categories extends _$Categories {
|
class Categories extends _$Categories {
|
||||||
@override
|
@override
|
||||||
Future<List<Category>> build() async {
|
Future<List<Category>> build() async {
|
||||||
|
|||||||
@@ -9,21 +9,24 @@ part of 'categories_provider.dart';
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
// ignore_for_file: type=lint, type=warning
|
// ignore_for_file: type=lint, type=warning
|
||||||
/// Provider for categories list with online-first approach
|
/// Provider for categories list with online-first approach
|
||||||
|
/// keepAlive ensures data persists when switching tabs
|
||||||
|
|
||||||
@ProviderFor(Categories)
|
@ProviderFor(Categories)
|
||||||
const categoriesProvider = CategoriesProvider._();
|
const categoriesProvider = CategoriesProvider._();
|
||||||
|
|
||||||
/// Provider for categories list with online-first approach
|
/// Provider for categories list with online-first approach
|
||||||
|
/// keepAlive ensures data persists when switching tabs
|
||||||
final class CategoriesProvider
|
final class CategoriesProvider
|
||||||
extends $AsyncNotifierProvider<Categories, List<Category>> {
|
extends $AsyncNotifierProvider<Categories, List<Category>> {
|
||||||
/// Provider for categories list with online-first approach
|
/// Provider for categories list with online-first approach
|
||||||
|
/// keepAlive ensures data persists when switching tabs
|
||||||
const CategoriesProvider._()
|
const CategoriesProvider._()
|
||||||
: super(
|
: super(
|
||||||
from: null,
|
from: null,
|
||||||
argument: null,
|
argument: null,
|
||||||
retry: null,
|
retry: null,
|
||||||
name: r'categoriesProvider',
|
name: r'categoriesProvider',
|
||||||
isAutoDispose: true,
|
isAutoDispose: false,
|
||||||
dependencies: null,
|
dependencies: null,
|
||||||
$allTransitiveDependencies: null,
|
$allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
@@ -36,9 +39,10 @@ final class CategoriesProvider
|
|||||||
Categories create() => Categories();
|
Categories create() => Categories();
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$categoriesHash() => r'33c33b08f8926e5bbbd112285591c74a3ff0f61c';
|
String _$categoriesHash() => r'c26eb4b4a76ce796eb65b7843a390805528dec4a';
|
||||||
|
|
||||||
/// Provider for categories list with online-first approach
|
/// Provider for categories list with online-first approach
|
||||||
|
/// keepAlive ensures data persists when switching tabs
|
||||||
|
|
||||||
abstract class _$Categories extends $AsyncNotifier<List<Category>> {
|
abstract class _$Categories extends $AsyncNotifier<List<Category>> {
|
||||||
FutureOr<List<Category>> build();
|
FutureOr<List<Category>> build();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import '../../domain/entities/category.dart';
|
import '../../domain/entities/category.dart';
|
||||||
import '../pages/category_detail_page.dart';
|
|
||||||
|
|
||||||
/// Category card widget
|
/// Category card widget
|
||||||
class CategoryCard extends StatelessWidget {
|
class CategoryCard extends StatelessWidget {
|
||||||
@@ -22,12 +22,7 @@ class CategoryCard extends StatelessWidget {
|
|||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// Navigate to category detail page
|
// Navigate to category detail page
|
||||||
Navigator.push(
|
context.push('/categories/${category.id}');
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => CategoryDetailPage(category: category),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import '../../domain/entities/product.dart';
|
import '../../domain/entities/product.dart';
|
||||||
import '../../data/models/product_model.dart';
|
import '../../data/models/product_model.dart';
|
||||||
import '../providers/products_provider.dart';
|
import '../providers/products_provider.dart';
|
||||||
@@ -139,7 +140,7 @@ class _BatchUpdatePageState extends ConsumerState<BatchUpdatePage> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
onPressed: _isLoading ? null : () => Navigator.pop(context),
|
onPressed: _isLoading ? null : () => context.pop(),
|
||||||
child: const Text('Cancel'),
|
child: const Text('Cancel'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -327,7 +328,7 @@ class _BatchUpdatePageState extends ConsumerState<BatchUpdatePage> {
|
|||||||
ref.invalidate(productsProvider);
|
ref.invalidate(productsProvider);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
|
|||||||
@@ -3,33 +3,66 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import '../../domain/entities/product.dart';
|
import '../../domain/entities/product.dart';
|
||||||
|
import '../providers/products_provider.dart';
|
||||||
import '../../../categories/presentation/providers/categories_provider.dart';
|
import '../../../categories/presentation/providers/categories_provider.dart';
|
||||||
import '../../../../shared/widgets/price_display.dart';
|
import '../../../../shared/widgets/price_display.dart';
|
||||||
|
|
||||||
/// Product detail page showing full product information
|
/// Product detail page showing full product information
|
||||||
class ProductDetailPage extends ConsumerWidget {
|
class ProductDetailPage extends ConsumerWidget {
|
||||||
final Product product;
|
final String productId;
|
||||||
|
|
||||||
const ProductDetailPage({
|
const ProductDetailPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.product,
|
required this.productId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final productsAsync = ref.watch(productsProvider);
|
||||||
final categoriesAsync = ref.watch(categoriesProvider);
|
final categoriesAsync = ref.watch(categoriesProvider);
|
||||||
|
|
||||||
// Find category name
|
return productsAsync.when(
|
||||||
final categoryName = categoriesAsync.whenOrNull(
|
data: (products) {
|
||||||
data: (categories) {
|
// Find the product by ID
|
||||||
final category = categories.firstWhere(
|
Product? product;
|
||||||
(cat) => cat.id == product.categoryId,
|
try {
|
||||||
orElse: () => categories.first,
|
product = products.firstWhere((p) => p.id == productId);
|
||||||
);
|
} catch (e) {
|
||||||
return category.name;
|
// Product not found
|
||||||
},
|
return Scaffold(
|
||||||
);
|
appBar: AppBar(title: const Text('Product Not Found')),
|
||||||
|
body: const Center(child: Text('Product not found')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find category name
|
||||||
|
final categoryName = categoriesAsync.whenOrNull(
|
||||||
|
data: (categories) {
|
||||||
|
try {
|
||||||
|
final category = categories.firstWhere(
|
||||||
|
(cat) => cat.id == product!.categoryId,
|
||||||
|
);
|
||||||
|
return category.name;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return _buildProductDetail(context, product, categoryName);
|
||||||
|
},
|
||||||
|
loading: () => Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Product Details')),
|
||||||
|
body: const Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
|
error: (error, stack) => Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Error')),
|
||||||
|
body: Center(child: Text('Error: $error')),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildProductDetail(BuildContext context, Product product, String? categoryName) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Product Details'),
|
title: const Text('Product Details'),
|
||||||
@@ -48,7 +81,7 @@ class ProductDetailPage extends ConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Product Image
|
// Product Image
|
||||||
_buildProductImage(context),
|
_buildProductImage(context, product),
|
||||||
|
|
||||||
// Product Info Section
|
// Product Info Section
|
||||||
Padding(
|
Padding(
|
||||||
@@ -97,19 +130,19 @@ class ProductDetailPage extends ConsumerWidget {
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Stock Information
|
// Stock Information
|
||||||
_buildStockSection(context),
|
_buildStockSection(context, product),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Description Section
|
// Description Section
|
||||||
_buildDescriptionSection(context),
|
_buildDescriptionSection(context, product),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Additional Information
|
// Additional Information
|
||||||
_buildAdditionalInfo(context),
|
_buildAdditionalInfo(context, product),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Action Buttons
|
// Action Buttons
|
||||||
_buildActionButtons(context),
|
_buildActionButtons(context, product),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -120,7 +153,7 @@ class ProductDetailPage extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build product image section
|
/// Build product image section
|
||||||
Widget _buildProductImage(BuildContext context) {
|
Widget _buildProductImage(BuildContext context, Product product) {
|
||||||
return Hero(
|
return Hero(
|
||||||
tag: 'product-${product.id}',
|
tag: 'product-${product.id}',
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -154,9 +187,9 @@ class ProductDetailPage extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build stock information section
|
/// Build stock information section
|
||||||
Widget _buildStockSection(BuildContext context) {
|
Widget _buildStockSection(BuildContext context, Product product) {
|
||||||
final stockColor = _getStockColor(context);
|
final stockColor = _getStockColor(context, product);
|
||||||
final stockStatus = _getStockStatus();
|
final stockStatus = _getStockStatus(product);
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -245,7 +278,7 @@ class ProductDetailPage extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build description section
|
/// Build description section
|
||||||
Widget _buildDescriptionSection(BuildContext context) {
|
Widget _buildDescriptionSection(BuildContext context, Product product) {
|
||||||
if (product.description == null || product.description!.isEmpty) {
|
if (product.description == null || product.description!.isEmpty) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
@@ -269,7 +302,7 @@ class ProductDetailPage extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build additional information section
|
/// Build additional information section
|
||||||
Widget _buildAdditionalInfo(BuildContext context) {
|
Widget _buildAdditionalInfo(BuildContext context, Product product) {
|
||||||
final dateFormat = DateFormat('MMM dd, yyyy');
|
final dateFormat = DateFormat('MMM dd, yyyy');
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
@@ -355,7 +388,7 @@ class ProductDetailPage extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build action buttons
|
/// Build action buttons
|
||||||
Widget _buildActionButtons(BuildContext context) {
|
Widget _buildActionButtons(BuildContext context, Product product) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
// Add to Cart Button
|
// Add to Cart Button
|
||||||
@@ -400,7 +433,7 @@ class ProductDetailPage extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get stock color based on quantity
|
/// Get stock color based on quantity
|
||||||
Color _getStockColor(BuildContext context) {
|
Color _getStockColor(BuildContext context, Product product) {
|
||||||
if (product.stockQuantity == 0) {
|
if (product.stockQuantity == 0) {
|
||||||
return Colors.red;
|
return Colors.red;
|
||||||
} else if (product.stockQuantity < 5) {
|
} else if (product.stockQuantity < 5) {
|
||||||
@@ -411,7 +444,7 @@ class ProductDetailPage extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get stock status text
|
/// Get stock status text
|
||||||
String _getStockStatus() {
|
String _getStockStatus(Product product) {
|
||||||
if (product.stockQuantity == 0) {
|
if (product.stockQuantity == 0) {
|
||||||
return 'Out of Stock';
|
return 'Out of Stock';
|
||||||
} else if (product.stockQuantity < 5) {
|
} else if (product.stockQuantity < 5) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import '../widgets/product_grid.dart';
|
import '../widgets/product_grid.dart';
|
||||||
import '../widgets/product_search_bar.dart';
|
import '../widgets/product_search_bar.dart';
|
||||||
import '../widgets/product_list_item.dart';
|
import '../widgets/product_list_item.dart';
|
||||||
@@ -99,13 +100,9 @@ class _ProductsPageState extends ConsumerState<ProductsPage> {
|
|||||||
final selectedProducts = filteredProducts
|
final selectedProducts = filteredProducts
|
||||||
.where((p) => _selectedProductIds.contains(p.id))
|
.where((p) => _selectedProductIds.contains(p.id))
|
||||||
.toList();
|
.toList();
|
||||||
Navigator.push(
|
context.push(
|
||||||
context,
|
'/products/batch-update',
|
||||||
MaterialPageRoute(
|
extra: selectedProducts,
|
||||||
builder: (context) => BatchUpdatePage(
|
|
||||||
selectedProducts: selectedProducts,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).then((_) {
|
).then((_) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isSelectionMode = false;
|
_isSelectionMode = false;
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import '../../../../core/providers/providers.dart';
|
|||||||
part 'products_provider.g.dart';
|
part 'products_provider.g.dart';
|
||||||
|
|
||||||
/// Provider for products list with online-first approach
|
/// Provider for products list with online-first approach
|
||||||
@riverpod
|
/// keepAlive ensures data persists when switching tabs
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
class Products extends _$Products {
|
class Products extends _$Products {
|
||||||
@override
|
@override
|
||||||
Future<List<Product>> build() async {
|
Future<List<Product>> build() async {
|
||||||
|
|||||||
@@ -9,21 +9,24 @@ part of 'products_provider.dart';
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
// ignore_for_file: type=lint, type=warning
|
// ignore_for_file: type=lint, type=warning
|
||||||
/// Provider for products list with online-first approach
|
/// Provider for products list with online-first approach
|
||||||
|
/// keepAlive ensures data persists when switching tabs
|
||||||
|
|
||||||
@ProviderFor(Products)
|
@ProviderFor(Products)
|
||||||
const productsProvider = ProductsProvider._();
|
const productsProvider = ProductsProvider._();
|
||||||
|
|
||||||
/// Provider for products list with online-first approach
|
/// Provider for products list with online-first approach
|
||||||
|
/// keepAlive ensures data persists when switching tabs
|
||||||
final class ProductsProvider
|
final class ProductsProvider
|
||||||
extends $AsyncNotifierProvider<Products, List<Product>> {
|
extends $AsyncNotifierProvider<Products, List<Product>> {
|
||||||
/// Provider for products list with online-first approach
|
/// Provider for products list with online-first approach
|
||||||
|
/// keepAlive ensures data persists when switching tabs
|
||||||
const ProductsProvider._()
|
const ProductsProvider._()
|
||||||
: super(
|
: super(
|
||||||
from: null,
|
from: null,
|
||||||
argument: null,
|
argument: null,
|
||||||
retry: null,
|
retry: null,
|
||||||
name: r'productsProvider',
|
name: r'productsProvider',
|
||||||
isAutoDispose: true,
|
isAutoDispose: false,
|
||||||
dependencies: null,
|
dependencies: null,
|
||||||
$allTransitiveDependencies: null,
|
$allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
@@ -36,9 +39,10 @@ final class ProductsProvider
|
|||||||
Products create() => Products();
|
Products create() => Products();
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$productsHash() => r'0ff8c2de46bb4b1e29678cc811ec121c9fb4c8eb';
|
String _$productsHash() => r'1fa5341d86a35a3b3d6666da88e0c5db757cdcdb';
|
||||||
|
|
||||||
/// Provider for products list with online-first approach
|
/// Provider for products list with online-first approach
|
||||||
|
/// keepAlive ensures data persists when switching tabs
|
||||||
|
|
||||||
abstract class _$Products extends $AsyncNotifier<List<Product>> {
|
abstract class _$Products extends $AsyncNotifier<List<Product>> {
|
||||||
FutureOr<List<Product>> build();
|
FutureOr<List<Product>> build();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import '../../domain/entities/product.dart';
|
import '../../domain/entities/product.dart';
|
||||||
import '../pages/product_detail_page.dart';
|
|
||||||
import '../../../../shared/widgets/price_display.dart';
|
import '../../../../shared/widgets/price_display.dart';
|
||||||
|
|
||||||
/// Product card widget
|
/// Product card widget
|
||||||
@@ -20,12 +20,7 @@ class ProductCard extends StatelessWidget {
|
|||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// Navigate to product detail page
|
// Navigate to product detail page
|
||||||
Navigator.push(
|
context.push('/products/${product.id}');
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => ProductDetailPage(product: product),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import '../../domain/entities/product.dart';
|
import '../../domain/entities/product.dart';
|
||||||
import '../pages/product_detail_page.dart';
|
|
||||||
import '../../../../shared/widgets/price_display.dart';
|
import '../../../../shared/widgets/price_display.dart';
|
||||||
|
|
||||||
/// Product list item widget for list view
|
/// Product list item widget for list view
|
||||||
@@ -23,12 +23,7 @@ class ProductListItem extends StatelessWidget {
|
|||||||
onTap: onTap ??
|
onTap: onTap ??
|
||||||
() {
|
() {
|
||||||
// Navigate to product detail page
|
// Navigate to product detail page
|
||||||
Navigator.push(
|
context.push('/products/${product.id}');
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => ProductDetailPage(product: product),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(12.0),
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import '../providers/settings_provider.dart';
|
import '../providers/settings_provider.dart';
|
||||||
import '../../../auth/presentation/providers/auth_provider.dart';
|
import '../../../auth/presentation/providers/auth_provider.dart';
|
||||||
import '../../../../core/constants/app_constants.dart';
|
import '../../../../core/constants/app_constants.dart';
|
||||||
@@ -105,11 +106,11 @@ class SettingsPage extends ConsumerWidget {
|
|||||||
content: const Text('Are you sure you want to logout?'),
|
content: const Text('Are you sure you want to logout?'),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context, false),
|
onPressed: () => context.pop(false),
|
||||||
child: const Text('Cancel'),
|
child: const Text('Cancel'),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () => Navigator.pop(context, true),
|
onPressed: () => context.pop(true),
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
backgroundColor: Theme.of(context).colorScheme.error,
|
backgroundColor: Theme.of(context).colorScheme.error,
|
||||||
),
|
),
|
||||||
@@ -294,7 +295,7 @@ class SettingsPage extends ConsumerWidget {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
ref.read(settingsProvider.notifier).updateTheme(value);
|
ref.read(settingsProvider.notifier).updateTheme(value);
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -305,7 +306,7 @@ class SettingsPage extends ConsumerWidget {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
ref.read(settingsProvider.notifier).updateTheme(value);
|
ref.read(settingsProvider.notifier).updateTheme(value);
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -316,7 +317,7 @@ class SettingsPage extends ConsumerWidget {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
ref.read(settingsProvider.notifier).updateTheme(value);
|
ref.read(settingsProvider.notifier).updateTheme(value);
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -341,7 +342,7 @@ class SettingsPage extends ConsumerWidget {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
ref.read(settingsProvider.notifier).updateLanguage(value);
|
ref.read(settingsProvider.notifier).updateLanguage(value);
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -352,7 +353,7 @@ class SettingsPage extends ConsumerWidget {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
ref.read(settingsProvider.notifier).updateLanguage(value);
|
ref.read(settingsProvider.notifier).updateLanguage(value);
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -363,7 +364,7 @@ class SettingsPage extends ConsumerWidget {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
ref.read(settingsProvider.notifier).updateLanguage(value);
|
ref.read(settingsProvider.notifier).updateLanguage(value);
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -388,7 +389,7 @@ class SettingsPage extends ConsumerWidget {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
// TODO: Implement currency update
|
// TODO: Implement currency update
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -399,7 +400,7 @@ class SettingsPage extends ConsumerWidget {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
// TODO: Implement currency update
|
// TODO: Implement currency update
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -410,7 +411,7 @@ class SettingsPage extends ConsumerWidget {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
// TODO: Implement currency update
|
// TODO: Implement currency update
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -437,13 +438,13 @@ class SettingsPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => context.pop(),
|
||||||
child: const Text('Cancel'),
|
child: const Text('Cancel'),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// TODO: Implement store name update
|
// TODO: Implement store name update
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Store name updated')),
|
const SnackBar(content: Text('Store name updated')),
|
||||||
);
|
);
|
||||||
@@ -476,13 +477,13 @@ class SettingsPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => context.pop(),
|
||||||
child: const Text('Cancel'),
|
child: const Text('Cancel'),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// TODO: Implement tax rate update
|
// TODO: Implement tax rate update
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Tax rate updated')),
|
const SnackBar(content: Text('Tax rate updated')),
|
||||||
);
|
);
|
||||||
@@ -521,7 +522,7 @@ class SettingsPage extends ConsumerWidget {
|
|||||||
|
|
||||||
// Close loading dialog
|
// Close loading dialog
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Data synced successfully')),
|
const SnackBar(content: Text('Data synced successfully')),
|
||||||
);
|
);
|
||||||
@@ -538,12 +539,12 @@ class SettingsPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => context.pop(),
|
||||||
child: const Text('Cancel'),
|
child: const Text('Cancel'),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Cache cleared')),
|
const SnackBar(content: Text('Cache cleared')),
|
||||||
);
|
);
|
||||||
|
|||||||
81
lib/shared/widgets/app_bottom_nav_shell.dart
Normal file
81
lib/shared/widgets/app_bottom_nav_shell.dart
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
/// Shell widget that provides bottom navigation for the app
|
||||||
|
class AppBottomNavShell extends StatelessWidget {
|
||||||
|
const AppBottomNavShell({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: child,
|
||||||
|
bottomNavigationBar: _AppBottomNavigationBar(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppBottomNavigationBar extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final location = GoRouterState.of(context).matchedLocation;
|
||||||
|
|
||||||
|
// Determine current index based on location
|
||||||
|
int currentIndex = 0;
|
||||||
|
if (location == '/') {
|
||||||
|
currentIndex = 0;
|
||||||
|
} else if (location.startsWith('/products')) {
|
||||||
|
currentIndex = 1;
|
||||||
|
} else if (location.startsWith('/categories')) {
|
||||||
|
currentIndex = 2;
|
||||||
|
} else if (location.startsWith('/settings')) {
|
||||||
|
currentIndex = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NavigationBar(
|
||||||
|
selectedIndex: currentIndex,
|
||||||
|
onDestinationSelected: (index) {
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
context.go('/');
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
context.go('/products');
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
context.go('/categories');
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
context.go('/settings');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
destinations: const [
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.point_of_sale_outlined),
|
||||||
|
selectedIcon: Icon(Icons.point_of_sale),
|
||||||
|
label: 'Home',
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.inventory_2_outlined),
|
||||||
|
selectedIcon: Icon(Icons.inventory_2),
|
||||||
|
label: 'Products',
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.category_outlined),
|
||||||
|
selectedIcon: Icon(Icons.category),
|
||||||
|
label: 'Categories',
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.settings_outlined),
|
||||||
|
selectedIcon: Icon(Icons.settings),
|
||||||
|
label: 'Settings',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -480,6 +480,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
version: "2.1.3"
|
||||||
|
go_router:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: go_router
|
||||||
|
sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "14.8.1"
|
||||||
graphs:
|
graphs:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ dependencies:
|
|||||||
flutter_riverpod: ^3.0.0
|
flutter_riverpod: ^3.0.0
|
||||||
riverpod_annotation: ^3.0.0
|
riverpod_annotation: ^3.0.0
|
||||||
|
|
||||||
|
# Routing
|
||||||
|
go_router: ^14.6.2
|
||||||
|
|
||||||
# Network
|
# Network
|
||||||
dio: ^5.7.0
|
dio: ^5.7.0
|
||||||
connectivity_plus: ^6.1.1
|
connectivity_plus: ^6.1.1
|
||||||
|
|||||||
Reference in New Issue
Block a user