feat: Implement FAQs feature for staff application with updated routing and UI components
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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,81 +13,20 @@ 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,
|
||||
),
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
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: BlocProvider<FaqsBloc>(
|
||||
create: (BuildContext context) =>
|
||||
Modular.get<FaqsBloc>()..add(const FetchFaqsEvent()),
|
||||
child: const Stack(children: <Widget>[FaqsWidget()]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user