feat: Adds operation pages and refactors navigation

Introduces dedicated pages and routes for Delivery and Inventory operations.

Refactors the application drawer to include navigation links for these new operations, ensuring proper route transitions and closing the drawer on tap. Enhances the drawer's active state styling for list and expansion tiles, providing better visual feedback.

Extracts common app bar logic into a reusable `MainAppbarComponent` and integrates it into operation pages, starting with Reception, to standardize the application's header.

Standardizes routing paths for operation pages and ensures consistent page transitions across them.
This commit is contained in:
mandreshope 2025-07-24 11:07:10 +03:00
parent 48556dbeed
commit 4682be5b62
9 changed files with 322 additions and 65 deletions

View File

@ -4,3 +4,4 @@ export 'primary_button_component.dart';
export 'product_scanned_component.dart';
export 'outline_button_component.dart';
export 'quick_action_component.dart';
export 'main_appbar_component.dart';

View File

@ -88,26 +88,34 @@ class _DrawerComponentState extends ConsumerState<DrawerComponent> {
isActive: selected.isSelected(menu: 0),
children: [
_ListTile(
onTap: () {
Navigator.of(context).pop();
ReceptionRoute().go(context);
},
title: 'Réceptions',
isActive: selected.isSelected(menu: 0, sub: 0),
leadingIcon: Icons.move_to_inbox,
contentPadding: const EdgeInsets.symmetric(horizontal: 40),
),
_ListTile(
onTap: () {
Navigator.of(context).pop();
DeliveryRoute().go(context);
},
title: 'Livraisons',
isActive: selected.isSelected(menu: 0, sub: 1),
leadingIcon: Icons.local_shipping,
contentPadding: const EdgeInsets.symmetric(horizontal: 40),
),
_ListTile(
onTap: () {
Navigator.of(context).pop();
InventoryRoute().go(context);
},
title: 'Inventaires',
isActive: selected.isSelected(menu: 0, sub: 2),
leadingIcon: Icons.inventory,
contentPadding: const EdgeInsets.symmetric(horizontal: 40),
onTap: () {
Navigator.of(context).pop();
ProductListRoute().push(context);
},
),
],
),
@ -218,11 +226,15 @@ class _ListTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListTile(
selected: isActive,
selectedTileColor: AppTheme.of(context).primary.withValues(alpha: 0.1),
onTap: onTap,
contentPadding: contentPadding,
leading: Icon(
leadingIcon,
color: isActive ? AppTheme.of(context).primary : null,
color: isActive
? AppTheme.of(context).primary
: AppTheme.of(context).primaryText,
),
title: Text(
title,
@ -253,32 +265,39 @@ class _ExpansionTileComponent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ExpansionTile(
initiallyExpanded: isExpanded,
onExpansionChanged: onExpansionChanged,
tilePadding: const EdgeInsetsDirectional.symmetric(horizontal: 10),
shape: LinearBorder.none,
title: Row(
children: [
Icon(
leadingIcon,
color: isActive ? AppTheme.of(context).primary : null,
),
const SizedBox(width: 12),
Expanded(
child: Text(
title,
style: AppTheme.of(context).bodyLarge.copyWith(
color: isActive ? AppTheme.of(context).primary : null,
return Container(
color: isActive
? AppTheme.of(context).primary.withValues(alpha: 0.05)
: null,
child: ExpansionTile(
initiallyExpanded: isExpanded,
onExpansionChanged: onExpansionChanged,
tilePadding: const EdgeInsetsDirectional.symmetric(horizontal: 10),
shape: LinearBorder.none,
title: Row(
children: [
Icon(
leadingIcon,
color: isActive
? AppTheme.of(context).primary
: AppTheme.of(context).primaryText,
),
const SizedBox(width: 12),
Expanded(
child: Text(
title,
style: AppTheme.of(context).bodyLarge.copyWith(
color: isActive ? AppTheme.of(context).primary : null,
),
),
),
),
],
],
),
trailing: Icon(
isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_right,
),
children: children,
),
trailing: Icon(
isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_right,
),
children: children,
);
}
}

View File

@ -0,0 +1,57 @@
import 'package:barcode_scanner/themes/app_theme.dart';
import 'package:flutter/material.dart';
class MainAppbarComponent extends StatelessWidget
implements PreferredSizeWidget {
const MainAppbarComponent({
super.key,
this.title,
this.subTitle,
this.scaffoledKey,
});
final GlobalKey<ScaffoldState>? scaffoledKey;
final String? title;
final String? subTitle;
@override
Widget build(BuildContext context) {
return AppBar(
leading: IconButton(
icon: Icon(Icons.menu, color: AppTheme.of(context).white),
onPressed: () {
scaffoledKey?.currentState?.openDrawer();
},
),
title: title == null || subTitle == null
? null
: Row(
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title ?? '',
style: AppTheme.of(
context,
).titleMedium.copyWith(color: AppTheme.of(context).white),
),
Text(
subTitle ?? '',
style: AppTheme.of(
context,
).bodyMedium.copyWith(color: AppTheme.of(context).white),
),
],
),
],
),
toolbarHeight: 60,
backgroundColor: AppTheme.of(context).primary,
elevation: 4,
);
}
@override
Size get preferredSize => Size.fromHeight(kToolbarHeight);
}

View File

@ -0,0 +1,59 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:barcode_scanner/components/components.dart';
import 'package:barcode_scanner/themes/app_theme.dart';
import 'package:flutter/material.dart';
class DeliveryPage extends ConsumerStatefulWidget {
const DeliveryPage({super.key});
@override
ConsumerState<DeliveryPage> createState() => _DeliveryPageState();
}
class _DeliveryPageState extends ConsumerState<DeliveryPage> {
final globalKey = GlobalKey<ScaffoldState>();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.of(context).primaryBackground,
key: globalKey,
drawer: DrawerComponent(
isOperationExpanded: true,
selectedState: SelectedDrawerState(menuIndex: 0, subMenuIndex: 1),
),
appBar: MainAppbarComponent(
scaffoledKey: globalKey,
title: "Livraisons",
subTitle: "Opérations d'Entrepôt",
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: ListView(
children: [
const SizedBox(height: 16),
QuickActionComponent(),
const SizedBox(height: 16),
Card(
color: AppTheme.of(context).secondaryBackground,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: BorderSide(color: AppTheme.of(context).alternate),
),
child: Padding(
padding: EdgeInsets.all(16),
child: Text(
'Activité Récente',
style: AppTheme.of(
context,
).bodyMedium.copyWith(fontWeight: FontWeight.bold),
),
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,59 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:barcode_scanner/components/components.dart';
import 'package:barcode_scanner/themes/app_theme.dart';
import 'package:flutter/material.dart';
class InventoryPage extends ConsumerStatefulWidget {
const InventoryPage({super.key});
@override
ConsumerState<InventoryPage> createState() => _InventoryPageState();
}
class _InventoryPageState extends ConsumerState<InventoryPage> {
final globalKey = GlobalKey<ScaffoldState>();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.of(context).primaryBackground,
key: globalKey,
drawer: DrawerComponent(
isOperationExpanded: true,
selectedState: SelectedDrawerState(menuIndex: 0, subMenuIndex: 2),
),
appBar: MainAppbarComponent(
scaffoledKey: globalKey,
title: "Inventaires",
subTitle: "Opérations d'Entrepôt",
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: ListView(
children: [
const SizedBox(height: 16),
QuickActionComponent(),
const SizedBox(height: 16),
Card(
color: AppTheme.of(context).secondaryBackground,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: BorderSide(color: AppTheme.of(context).alternate),
),
child: Padding(
padding: EdgeInsets.all(16),
child: Text(
'Activité Récente',
style: AppTheme.of(
context,
).bodyMedium.copyWith(fontWeight: FontWeight.bold),
),
),
),
],
),
),
);
}
}

View File

@ -1 +1,3 @@
export 'reception/reception_page.dart';
export 'delivery/delivery_page.dart';
export 'inventory/inventory_page.dart';

View File

@ -1,5 +1,4 @@
import 'package:barcode_scanner/components/components.dart';
import 'package:barcode_scanner/components/drawer_component.dart';
import 'package:barcode_scanner/pages/operation/reception/reception_page_model.dart';
import 'package:barcode_scanner/themes/app_theme.dart';
import 'package:flutter/scheduler.dart';
@ -29,38 +28,10 @@ class _ReceptionPageState extends ConsumerState<ReceptionPage> {
backgroundColor: AppTheme.of(context).primaryBackground,
key: globalKey,
drawer: DrawerComponent(isOperationExpanded: true),
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.menu, color: AppTheme.of(context).white),
onPressed: () {
globalKey.currentState?.openDrawer();
},
),
title: Row(
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Réceptions",
style: AppTheme.of(
context,
).titleMedium.copyWith(color: AppTheme.of(context).white),
),
Text(
"Opérations d'Entrepôt",
style: AppTheme.of(
context,
).bodyMedium.copyWith(color: AppTheme.of(context).white),
),
],
),
],
),
toolbarHeight: 60,
backgroundColor: AppTheme.of(context).primary,
elevation: 4,
appBar: MainAppbarComponent(
scaffoledKey: globalKey,
title: "Réceptions",
subTitle: "Opérations d'Entrepôt",
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),

View File

@ -2,6 +2,7 @@ import 'package:barcode_scanner/pages/login/login_page_model.dart';
import 'package:barcode_scanner/provider_container.dart';
import 'package:barcode_scanner/router/go_router_builder.dart';
import 'package:barcode_scanner/pages/pages.dart';
import 'package:barcode_scanner/router/router.dart';
import 'package:flutter/material.dart';
export 'package:go_router/go_router.dart';
part 'go_secure_router_builder.g.dart';
@ -16,7 +17,9 @@ final appSecureRoutes = $appRoutes;
TypedGoRoute<ProductFormRoute>(path: 'ProductFormPage'),
TypedGoRoute<ProductListRoute>(path: 'ProductListPage'),
TypedGoRoute<ProfileRoute>(path: 'ProfilePage'),
TypedGoRoute<ReceptionRoute>(path: 'ReceptionRoute'),
TypedGoRoute<ReceptionRoute>(path: 'ReceptionPage'),
TypedGoRoute<DeliveryRoute>(path: 'DeliveryPage'),
TypedGoRoute<InventoryRoute>(path: 'InventoryPage'),
],
)
class SecureRoute extends GoRouteData with _$SecureRoute {
@ -70,5 +73,40 @@ class ReceptionRoute extends GoRouteData with _$ReceptionRoute {
const ReceptionRoute();
@override
Widget build(BuildContext context, GoRouterState state) => ReceptionPage();
CustomTransitionPage<void> buildPage(
BuildContext context,
GoRouterState state,
) => buildPageWithDefaultTransition(
context: context,
state: state,
child: ReceptionPage(),
);
}
class DeliveryRoute extends GoRouteData with _$DeliveryRoute {
const DeliveryRoute();
@override
CustomTransitionPage<void> buildPage(
BuildContext context,
GoRouterState state,
) => buildPageWithDefaultTransition(
context: context,
state: state,
child: DeliveryPage(),
);
}
class InventoryRoute extends GoRouteData with _$InventoryRoute {
const InventoryRoute();
@override
CustomTransitionPage<void> buildPage(
BuildContext context,
GoRouterState state,
) => buildPageWithDefaultTransition(
context: context,
state: state,
child: InventoryPage(),
);
}

View File

@ -26,10 +26,20 @@ RouteBase get $secureRoute => GoRouteData.$route(
),
GoRouteData.$route(path: 'ProfilePage', factory: _$ProfileRoute._fromState),
GoRouteData.$route(
path: 'ReceptionRoute',
path: 'ReceptionPage',
factory: _$ReceptionRoute._fromState,
),
GoRouteData.$route(
path: 'DeliveryPage',
factory: _$DeliveryRoute._fromState,
),
GoRouteData.$route(
path: 'InventoryPage',
factory: _$InventoryRoute._fromState,
),
],
);
@ -145,7 +155,48 @@ mixin _$ReceptionRoute on GoRouteData {
const ReceptionRoute();
@override
String get location => GoRouteData.$location('/SecurePage/ReceptionRoute');
String get location => GoRouteData.$location('/SecurePage/ReceptionPage');
@override
void go(BuildContext context) => context.go(location);
@override
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
@override
void pushReplacement(BuildContext context) =>
context.pushReplacement(location);
@override
void replace(BuildContext context) => context.replace(location);
}
mixin _$DeliveryRoute on GoRouteData {
static DeliveryRoute _fromState(GoRouterState state) => const DeliveryRoute();
@override
String get location => GoRouteData.$location('/SecurePage/DeliveryPage');
@override
void go(BuildContext context) => context.go(location);
@override
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
@override
void pushReplacement(BuildContext context) =>
context.pushReplacement(location);
@override
void replace(BuildContext context) => context.replace(location);
}
mixin _$InventoryRoute on GoRouteData {
static InventoryRoute _fromState(GoRouterState state) =>
const InventoryRoute();
@override
String get location => GoRouteData.$location('/SecurePage/InventoryPage');
@override
void go(BuildContext context) => context.go(location);