feat: Implement FAQs feature for staff application with updated routing and UI components

This commit is contained in:
Achintha Isuru
2026-02-18 23:21:26 -05:00
parent 316a010779
commit 8578723fb3
12 changed files with 54 additions and 241 deletions

View File

@@ -203,7 +203,9 @@ class StaffPaths {
static const String leaderboard = '/leaderboard'; static const String leaderboard = '/leaderboard';
/// FAQs - frequently asked questions. /// FAQs - frequently asked questions.
static const String faqs = '/faqs'; ///
/// Access to frequently asked questions about the staff application.
static const String faqs = '/worker-main/faqs/';
// ========================================================================== // ==========================================================================
// PRIVACY & SECURITY // PRIVACY & SECURITY

View File

@@ -264,4 +264,7 @@ class UiIcons {
/// Chef hat icon for attire /// Chef hat icon for attire
static const IconData chefHat = _IconLib.chefHat; static const IconData chefHat = _IconLib.chefHat;
/// Help circle icon for FAQs
static const IconData helpCircle = _IconLib.helpCircle;
} }

View File

@@ -198,6 +198,11 @@ class StaffProfilePage extends StatelessWidget {
ProfileMenuGrid( ProfileMenuGrid(
crossAxisCount: 3, crossAxisCount: 3,
children: [ children: [
ProfileMenuItem(
icon: UiIcons.helpCircle,
label: i18n.header.title.contains("Perfil") ? "Preguntas Frecuentes" : "FAQs",
onTap: () => Modular.to.toFaqs(),
),
ProfileMenuItem( ProfileMenuItem(
icon: UiIcons.shield, icon: UiIcons.shield,
label: i18n.header.title.contains("Perfil") ? "Privacidad" : "Privacy & Security", label: i18n.header.title.contains("Perfil") ? "Privacidad" : "Privacy & Security",

View File

@@ -1,122 +0,0 @@
# Staff FAQs Feature
A modular feature package providing Frequently Asked Questions functionality for the staff mobile application.
## Architecture
This package follows clean architecture principles with clear separation of concerns:
### Domain Layer (`lib/src/domain/`)
- **Entities**: `FaqCategory`, `FaqItem` - Domain models representing FAQ data
- **Repositories**: `FaqsRepositoryInterface` - Abstract interface for FAQ data access
- **Use Cases**:
- `GetFaqsUseCase` - Retrieves all FAQs
- `SearchFaqsUseCase` - Searches FAQs by query
### Data Layer (`lib/src/data/`)
- **Repositories Implementation**: `FaqsRepositoryImpl`
- Loads FAQs from JSON asset file (`lib/src/assets/faqs/faqs.json`)
- Implements caching to avoid repeated file reads
- Provides search filtering logic
### Presentation Layer (`lib/src/presentation/`)
- **BLoC**: `FaqsBloc` - State management with events and states
- Events: `FetchFaqsEvent`, `SearchFaqsEvent`
- State: `FaqsState` - Manages categories, loading, search query, and errors
- **Pages**: `FaqsPage` - Full-screen FAQ view with AppBar and contact button
- **Widgets**: `FaqsWidget` - Reusable accordion widget with search functionality
## Features
**Search Functionality** - Real-time search across questions and answers
**Accordion UI** - Expandable/collapsible FAQ items
**Category Organization** - FAQs grouped by category
**Localization** - Support for English and Spanish
**Contact Support Button** - Direct link to messaging/support
**Empty State** - Helpful message when no results found
**Loading State** - Loading indicator while fetching FAQs
**Asset-based Data** - FAQ content stored in JSON for easy updates
## Data Structure
FAQs are stored in `lib/src/assets/faqs/faqs.json` with the following structure:
```json
[
{
"category": "Getting Started",
"questions": [
{
"q": "How do I apply for shifts?",
"a": "Browse available shifts..."
}
]
}
]
```
## Localization
Localization strings are defined in:
- `packages/core_localization/lib/src/l10n/en.i18n.json`
- `packages/core_localization/lib/src/l10n/es.i18n.json`
Available keys:
- `staff_faqs.title` - Page title
- `staff_faqs.search_placeholder` - Search input hint
- `staff_faqs.no_results` - Empty state message
- `staff_faqs.contact_support` - Button label
## Dependency Injection
The `FaqsModule` provides all dependencies:
```dart
FaqsModule().binds(injector);
```
Registered singletons:
- `FaqsRepositoryInterface``FaqsRepositoryImpl`
- `GetFaqsUseCase`
- `SearchFaqsUseCase`
- `FaqsBloc`
## Routing
Routes are defined in `FaqsModule.routes()`:
- `/``FaqsPage`
## Usage
1. Add `FaqsModule` to your modular configuration
2. Access the page via routing: `context.push('/faqs')`
3. The BLoC will automatically fetch FAQs on page load
## Asset Configuration
Update `pubspec.yaml` to include assets:
```yaml
flutter:
assets:
- lib/src/assets/faqs/
```
## Testing
The package includes test support with:
- `bloc_test` for BLoC testing
- `mocktail` for mocking dependencies
## Design System Integration
Uses the common design system components:
- `UiColors` - Color constants
- `UiConstants` - Sizing and radius constants
- `LucideIcons` - Icon library
## Notes
- FAQs are cached in memory after first load to improve performance
- Search is case-insensitive
- The widget state (expanded/collapsed items) is local to the widget and resets on navigation

View File

@@ -3,8 +3,6 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:go_router/go_router.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../blocs/faqs_bloc.dart'; import '../blocs/faqs_bloc.dart';
import '../widgets/faqs_widget.dart'; import '../widgets/faqs_widget.dart';
@@ -15,80 +13,19 @@ class FaqsPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider<FaqsBloc>( return Scaffold(
create: (BuildContext context) => Modular.get<FaqsBloc>()..add(const FetchFaqsEvent()), appBar: UiAppBar(
child: Scaffold( title: t.staff_faqs.title,
backgroundColor: UiColors.background, showBackButton: true,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: GestureDetector(
onTap: () => context.pop(),
child: const Icon(
LucideIcons.chevronLeft,
color: UiColors.textSecondary,
),
),
title: Text(
t.staff_faqs.title,
style: const TextStyle(
color: UiColors.textPrimary,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
bottom: PreferredSize( bottom: PreferredSize(
preferredSize: const Size.fromHeight(1), preferredSize: const Size.fromHeight(1),
child: Container(color: UiColors.border, height: 1), child: Container(color: UiColors.border, height: 1),
), ),
), ),
body: Stack( body: BlocProvider<FaqsBloc>(
children: <Widget>[ create: (BuildContext context) =>
const FaqsWidget(), Modular.get<FaqsBloc>()..add(const FetchFaqsEvent()),
// Contact Support Button at Bottom child: const Stack(children: <Widget>[FaqsWidget()]),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
padding: const EdgeInsets.all(20),
decoration: const BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: UiColors.border)),
),
child: SafeArea(
top: false,
child: SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: () => context.push('/messages'),
style: ElevatedButton.styleFrom(
backgroundColor: UiColors.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
),
elevation: 0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Icon(LucideIcons.messageCircle, size: 20),
const SizedBox(width: 8),
Text(
t.staff_faqs.contact_support,
style: const TextStyle(fontWeight: FontWeight.w600),
),
],
),
),
),
),
),
),
],
),
), ),
); );
} }

View File

@@ -2,7 +2,6 @@ import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:staff_faqs/src/presentation/blocs/faqs_bloc.dart'; import 'package:staff_faqs/src/presentation/blocs/faqs_bloc.dart';
/// Widget displaying FAQs with search functionality and accordion items /// Widget displaying FAQs with search functionality and accordion items
@@ -65,7 +64,7 @@ class _FaqsWidgetState extends State<FaqsWidget> {
hintText: t.staff_faqs.search_placeholder, hintText: t.staff_faqs.search_placeholder,
hintStyle: const TextStyle(color: UiColors.textPlaceholder), hintStyle: const TextStyle(color: UiColors.textPlaceholder),
prefixIcon: const Icon( prefixIcon: const Icon(
LucideIcons.search, UiIcons.search,
color: UiColors.textSecondary, color: UiColors.textSecondary,
), ),
border: InputBorder.none, border: InputBorder.none,
@@ -87,7 +86,7 @@ class _FaqsWidgetState extends State<FaqsWidget> {
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
const Icon( const Icon(
LucideIcons.helpCircle, UiIcons.helpCircle,
size: 48, size: 48,
color: UiColors.textSecondary, color: UiColors.textSecondary,
), ),
@@ -100,7 +99,9 @@ class _FaqsWidgetState extends State<FaqsWidget> {
), ),
) )
else else
...state.categories.asMap().entries.map((MapEntry<int, dynamic> entry) { ...state.categories.asMap().entries.map((
MapEntry<int, dynamic> entry,
) {
final int catIndex = entry.key; final int catIndex = entry.key;
final dynamic categoryItem = entry.value; final dynamic categoryItem = entry.value;
final String categoryName = categoryItem.category; final String categoryName = categoryItem.category;
@@ -118,7 +119,9 @@ class _FaqsWidgetState extends State<FaqsWidget> {
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
...questions.asMap().entries.map((MapEntry<int, dynamic> qEntry) { ...questions.asMap().entries.map((
MapEntry<int, dynamic> qEntry,
) {
final int qIndex = qEntry.key; final int qIndex = qEntry.key;
final dynamic questionItem = qEntry.value; final dynamic questionItem = qEntry.value;
final String key = '$catIndex-$qIndex'; final String key = '$catIndex-$qIndex';
@@ -128,16 +131,18 @@ class _FaqsWidgetState extends State<FaqsWidget> {
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: borderRadius: BorderRadius.circular(
BorderRadius.circular(UiConstants.radiusBase), UiConstants.radiusBase,
),
border: Border.all(color: UiColors.border), border: Border.all(color: UiColors.border),
), ),
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
InkWell( InkWell(
onTap: () => _toggleItem(key), onTap: () => _toggleItem(key),
borderRadius: borderRadius: BorderRadius.circular(
BorderRadius.circular(UiConstants.radiusBase), UiConstants.radiusBase,
),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Row( child: Row(
@@ -145,16 +150,13 @@ class _FaqsWidgetState extends State<FaqsWidget> {
Expanded( Expanded(
child: Text( child: Text(
questionItem.question, questionItem.question,
style: const TextStyle( style: UiTypography.body1r,
fontWeight: FontWeight.w500,
color: UiColors.textPrimary,
),
), ),
), ),
Icon( Icon(
isOpen isOpen
? LucideIcons.chevronUp ? UiIcons.chevronUp
: LucideIcons.chevronDown, : UiIcons.chevronDown,
color: UiColors.textSecondary, color: UiColors.textSecondary,
size: 20, size: 20,
), ),
@@ -172,21 +174,17 @@ class _FaqsWidgetState extends State<FaqsWidget> {
), ),
child: Text( child: Text(
questionItem.answer, questionItem.answer,
style: const TextStyle( style: UiTypography.body1r.textSecondary,
color: UiColors.textSecondary,
fontSize: 14,
height: 1.5,
),
), ),
), ),
], ],
), ),
); );
}).toList(), }),
const SizedBox(height: 12), const SizedBox(height: 12),
], ],
); );
}).toList(), }),
], ],
), ),
); );

View File

@@ -1,4 +1,5 @@
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
import 'data/repositories_impl/faqs_repository_impl.dart'; import 'data/repositories_impl/faqs_repository_impl.dart';
import 'domain/repositories/faqs_repository_interface.dart'; import 'domain/repositories/faqs_repository_interface.dart';
@@ -44,7 +45,7 @@ class FaqsModule extends Module {
@override @override
void routes(RouteManager r) { void routes(RouteManager r) {
r.child( r.child(
'/', StaffPaths.childRoute(StaffPaths.faqs, StaffPaths.faqs),
child: (_) => const FaqsPage(), child: (_) => const FaqsPage(),
); );
} }

View File

@@ -14,8 +14,6 @@ dependencies:
flutter_bloc: ^8.1.0 flutter_bloc: ^8.1.0
flutter_modular: ^6.3.0 flutter_modular: ^6.3.0
equatable: ^2.0.5 equatable: ^2.0.5
go_router: ^14.0.0
lucide_icons: ^0.257.0
# Architecture Packages # Architecture Packages
krow_core: krow_core:
@@ -25,12 +23,6 @@ dependencies:
core_localization: core_localization:
path: ../../../../../core_localization path: ../../../../../core_localization
dev_dependencies:
flutter_test:
sdk: flutter
bloc_test: ^9.1.0
mocktail: ^1.0.0
flutter: flutter:
uses-material-design: true uses-material-design: true
assets: assets:

View File

@@ -18,6 +18,7 @@ import 'package:staff_profile_experience/staff_profile_experience.dart';
import 'package:staff_profile_info/staff_profile_info.dart'; import 'package:staff_profile_info/staff_profile_info.dart';
import 'package:staff_shifts/staff_shifts.dart'; import 'package:staff_shifts/staff_shifts.dart';
import 'package:staff_tax_forms/staff_tax_forms.dart'; import 'package:staff_tax_forms/staff_tax_forms.dart';
import 'package:staff_faqs/staff_faqs.dart';
import 'package:staff_time_card/staff_time_card.dart'; import 'package:staff_time_card/staff_time_card.dart';
class StaffMainModule extends Module { class StaffMainModule extends Module {
@@ -102,5 +103,9 @@ class StaffMainModule extends Module {
StaffPaths.childRoute(StaffPaths.main, StaffPaths.shiftDetailsRoute), StaffPaths.childRoute(StaffPaths.main, StaffPaths.shiftDetailsRoute),
module: ShiftDetailsModule(), module: ShiftDetailsModule(),
); );
r.module(
StaffPaths.childRoute(StaffPaths.main, StaffPaths.faqs),
module: FaqsModule(),
);
} }
} }

View File

@@ -573,14 +573,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.3" version: "2.1.3"
go_router:
dependency: transitive
description:
name: go_router
sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3
url: "https://pub.dev"
source: hosted
version: "14.8.1"
google_fonts: google_fonts:
dependency: transitive dependency: transitive
description: description:

View File

@@ -59,7 +59,7 @@ The application is broken down into several key functional modules:
| Component | Primary Responsibility | Example Task | | Component | Primary Responsibility | Example Task |
| :--- | :--- | :--- | | :--- | :--- | :--- |
| **Router (GoRouter)** | Navigation traffic cop | Directs the user from the "Login" screen to the "Home" dashboard upon success. | | **Router (Flutter Modular)** | Navigation traffic cop | Directs the user from the "Login" screen to the "Home" dashboard upon success. |
| **Screens (UI)** | Displaying information | Renders the "Create Order" form and captures the user's input for date and time. | | **Screens (UI)** | Displaying information | Renders the "Create Order" form and captures the user's input for date and time. |
| **Providers (Riverpod)** | Data management & State | Holds the list of today's active shifts so multiple screens can access it without reloading. | | **Providers (Riverpod)** | Data management & State | Holds the list of today's active shifts so multiple screens can access it without reloading. |
| **Widgets** | Reusable UI building blocks | A "Shift Card" widget that displays shift details effectively, used in multiple lists throughout the app. | | **Widgets** | Reusable UI building blocks | A "Shift Card" widget that displays shift details effectively, used in multiple lists throughout the app. |
@@ -91,7 +91,7 @@ While currently operating as a high-fidelity prototype with mock data, the archi
## 8. Key Design Decisions ## 8. Key Design Decisions
* **Flutter Framework:** chosen for its ability to produce high-performance, native-feeling apps for both iOS and Android from a single codebase, reducing development time and cost. * **Flutter Framework:** chosen for its ability to produce high-performance, native-feeling apps for both iOS and Android from a single codebase, reducing development time and cost.
* **GoRouter for Navigation:** A modern routing package that handles complex navigation scenarios (like deep linking and sub-routes) which are essential for a multi-layered app like this. * **Flutter Modular for Navigation:** A modern routing package that handles complex navigation scenarios (like deep linking and sub-routes) which are essential for a multi-layered app like this.
* **Riverpod for State Management:** A robust solution that catches programming errors at compile-time (while writing code) rather than run-time (while using the app), increasing app stability. * **Riverpod for State Management:** A robust solution that catches programming errors at compile-time (while writing code) rather than run-time (while using the app), increasing app stability.
* **Mock Data Services:** The decision to use extensive mock data allows for rapid UI/UX iteration and testing of business flows without waiting for the full backend infrastructure to be built. * **Mock Data Services:** The decision to use extensive mock data allows for rapid UI/UX iteration and testing of business flows without waiting for the full backend infrastructure to be built.
@@ -102,7 +102,7 @@ flowchart TD
direction TB direction TB
subgraph PresentationLayer["Presentation Layer (UI)"] subgraph PresentationLayer["Presentation Layer (UI)"]
direction TB direction TB
Router["GoRouter Navigation"] Router["Flutter Modular Navigation"]
subgraph FeatureModules["Feature Modules"] subgraph FeatureModules["Feature Modules"]
AuthUI["Auth Screens"] AuthUI["Auth Screens"]
DashUI["Dashboard & Home"] DashUI["Dashboard & Home"]

View File

@@ -98,7 +98,7 @@ flowchart TD
direction TB direction TB
subgraph PresentationLayer["Presentation Layer (UI)"] subgraph PresentationLayer["Presentation Layer (UI)"]
direction TB direction TB
Router["GoRouter Navigation"] Router["Flutter Modular Navigation"]
subgraph FeatureModules["Feature Modules"] subgraph FeatureModules["Feature Modules"]
AuthUI["Auth & Onboarding"] AuthUI["Auth & Onboarding"]
MarketUI["Marketplace & Jobs"] MarketUI["Marketplace & Jobs"]