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

View File

@@ -264,4 +264,7 @@ class UiIcons {
/// Chef hat icon for attire
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(
crossAxisCount: 3,
children: [
ProfileMenuItem(
icon: UiIcons.helpCircle,
label: i18n.header.title.contains("Perfil") ? "Preguntas Frecuentes" : "FAQs",
onTap: () => Modular.to.toFaqs(),
),
ProfileMenuItem(
icon: UiIcons.shield,
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_bloc/flutter_bloc.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 '../widgets/faqs_widget.dart';
@@ -15,80 +13,19 @@ class FaqsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider<FaqsBloc>(
create: (BuildContext context) => Modular.get<FaqsBloc>()..add(const FetchFaqsEvent()),
child: Scaffold(
backgroundColor: UiColors.background,
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,
),
),
return Scaffold(
appBar: UiAppBar(
title: t.staff_faqs.title,
showBackButton: true,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1),
child: Container(color: UiColors.border, height: 1),
),
),
body: Stack(
children: <Widget>[
const FaqsWidget(),
// Contact Support Button at Bottom
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),
),
],
),
),
),
),
),
),
],
),
body: BlocProvider<FaqsBloc>(
create: (BuildContext context) =>
Modular.get<FaqsBloc>()..add(const FetchFaqsEvent()),
child: const Stack(children: <Widget>[FaqsWidget()]),
),
);
}

View File

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

View File

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

View File

@@ -14,8 +14,6 @@ dependencies:
flutter_bloc: ^8.1.0
flutter_modular: ^6.3.0
equatable: ^2.0.5
go_router: ^14.0.0
lucide_icons: ^0.257.0
# Architecture Packages
krow_core:
@@ -25,12 +23,6 @@ dependencies:
core_localization:
path: ../../../../../core_localization
dev_dependencies:
flutter_test:
sdk: flutter
bloc_test: ^9.1.0
mocktail: ^1.0.0
flutter:
uses-material-design: true
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_shifts/staff_shifts.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';
class StaffMainModule extends Module {
@@ -102,5 +103,9 @@ class StaffMainModule extends Module {
StaffPaths.childRoute(StaffPaths.main, StaffPaths.shiftDetailsRoute),
module: ShiftDetailsModule(),
);
r.module(
StaffPaths.childRoute(StaffPaths.main, StaffPaths.faqs),
module: FaqsModule(),
);
}
}

View File

@@ -573,14 +573,6 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:

View File

@@ -59,7 +59,7 @@ The application is broken down into several key functional modules:
| 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. |
| **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. |
@@ -91,7 +91,7 @@ While currently operating as a high-fidelity prototype with mock data, the archi
## 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.
* **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.
* **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
subgraph PresentationLayer["Presentation Layer (UI)"]
direction TB
Router["GoRouter Navigation"]
Router["Flutter Modular Navigation"]
subgraph FeatureModules["Feature Modules"]
AuthUI["Auth Screens"]
DashUI["Dashboard & Home"]

View File

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