Merge pull request #437 from Oloodi/408-feature-implement-paidunpaid-breaks---client-app-frontend-development
FAQ feature implemented in the staff application
This commit is contained in:
@@ -65,6 +65,11 @@ This project uses a modular `Makefile` for all common tasks.
|
||||
- **[03-contributing.md](./docs/03-contributing.md)**: Guidelines for new developers and setup checklist.
|
||||
- **[04-sync-prototypes.md](./docs/04-sync-prototypes.md)**: How to sync prototypes for local dev and AI context.
|
||||
|
||||
### Mobile Development Documentation
|
||||
- **[MOBILE/01-architecture-principles.md](./docs/MOBILE/01-architecture-principles.md)**: Flutter clean architecture, package roles, and dependency flow.
|
||||
- **[MOBILE/02-design-system-usage.md](./docs/MOBILE/02-design-system-usage.md)**: Design system components and theming guidelines.
|
||||
- **[MOBILE/00-agent-development-rules.md](./docs/MOBILE/00-agent-development-rules.md)**: Rules and best practices for mobile development.
|
||||
|
||||
## 🤝 Contributing
|
||||
New to the team? Please read our **[Contributing Guide](./docs/03-contributing.md)** to get your environment set up and understand our workflow.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -521,7 +521,8 @@
|
||||
"compliance": "COMPLIANCE",
|
||||
"level_up": "LEVEL UP",
|
||||
"finance": "FINANCE",
|
||||
"support": "SUPPORT"
|
||||
"support": "SUPPORT",
|
||||
"settings": "SETTINGS"
|
||||
},
|
||||
"menu_items": {
|
||||
"personal_info": "Personal Info",
|
||||
@@ -543,7 +544,8 @@
|
||||
"timecard": "Timecard",
|
||||
"faqs": "FAQs",
|
||||
"privacy_security": "Privacy & Security",
|
||||
"messages": "Messages"
|
||||
"messages": "Messages",
|
||||
"language": "Language"
|
||||
},
|
||||
"bank_account_page": {
|
||||
"title": "Bank Account",
|
||||
@@ -1143,6 +1145,12 @@
|
||||
"profile_visibility_updated": "Profile visibility updated successfully!"
|
||||
}
|
||||
},
|
||||
"staff_faqs": {
|
||||
"title": "FAQs",
|
||||
"search_placeholder": "Search questions...",
|
||||
"no_results": "No matching questions found",
|
||||
"contact_support": "Contact Support"
|
||||
},
|
||||
"success": {
|
||||
"hub": {
|
||||
"created": "Hub created successfully!",
|
||||
|
||||
@@ -521,7 +521,8 @@
|
||||
"compliance": "CUMPLIMIENTO",
|
||||
"level_up": "MEJORAR NIVEL",
|
||||
"finance": "FINANZAS",
|
||||
"support": "SOPORTE"
|
||||
"support": "SOPORTE",
|
||||
"settings": "AJUSTES"
|
||||
},
|
||||
"menu_items": {
|
||||
"personal_info": "Información Personal",
|
||||
@@ -543,7 +544,8 @@
|
||||
"timecard": "Tarjeta de Tiempo",
|
||||
"faqs": "Preguntas Frecuentes",
|
||||
"privacy_security": "Privacidad y Seguridad",
|
||||
"messages": "Mensajes"
|
||||
"messages": "Mensajes",
|
||||
"language": "Idioma"
|
||||
},
|
||||
"bank_account_page": {
|
||||
"title": "Cuenta Bancaria",
|
||||
@@ -1143,6 +1145,12 @@
|
||||
"profile_visibility_updated": "¡Visibilidad del perfil actualizada exitosamente!"
|
||||
}
|
||||
},
|
||||
"staff_faqs": {
|
||||
"title": "Preguntas Frecuentes",
|
||||
"search_placeholder": "Buscar preguntas...",
|
||||
"no_results": "No se encontraron preguntas coincidentes",
|
||||
"contact_support": "Contactar Soporte"
|
||||
},
|
||||
"success": {
|
||||
"hub": {
|
||||
"created": "¡Hub creado exitosamente!",
|
||||
|
||||
@@ -21,8 +21,8 @@ class UiColors {
|
||||
/// Foreground color on primary background (#F7FAFC)
|
||||
static const Color primaryForeground = Color(0xFFF7FAFC);
|
||||
|
||||
/// Inverse primary color (#9FABF1)
|
||||
static const Color primaryInverse = Color(0xFF9FABF1);
|
||||
/// Inverse primary color (#0A39DF)
|
||||
static const Color primaryInverse = Color.fromARGB(23, 10, 56, 223);
|
||||
|
||||
/// Secondary background color (#F1F3F5)
|
||||
static const Color secondary = Color(0xFFF1F3F5);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -49,13 +49,17 @@ class WorkerHomePage extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
BlocBuilder<HomeCubit, HomeState>(
|
||||
buildWhen: (previous, current) => previous.staffName != current.staffName,
|
||||
buildWhen: (previous, current) =>
|
||||
previous.staffName != current.staffName,
|
||||
builder: (context, state) {
|
||||
return HomeHeader(userName: state.staffName);
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space4),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: UiConstants.space4,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
BlocBuilder<HomeCubit, HomeState>(
|
||||
@@ -67,7 +71,7 @@ class WorkerHomePage extends StatelessWidget {
|
||||
return PlaceholderBanner(
|
||||
title: bannersI18n.complete_profile_title,
|
||||
subtitle: bannersI18n.complete_profile_subtitle,
|
||||
bg: UiColors.bgHighlight,
|
||||
bg: UiColors.primaryInverse,
|
||||
accent: UiColors.primary,
|
||||
onTap: () {
|
||||
Modular.to.toProfile();
|
||||
@@ -135,7 +139,8 @@ class WorkerHomePage extends StatelessWidget {
|
||||
EmptyStateWidget(
|
||||
message: emptyI18n.no_shifts_today,
|
||||
actionLink: emptyI18n.find_shifts_cta,
|
||||
onAction: () => Modular.to.toShifts(initialTab: 'find'),
|
||||
onAction: () =>
|
||||
Modular.to.toShifts(initialTab: 'find'),
|
||||
)
|
||||
else
|
||||
Column(
|
||||
@@ -183,9 +188,7 @@ class WorkerHomePage extends StatelessWidget {
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
// Recommended Shifts
|
||||
SectionHeader(
|
||||
title: sectionsI18n.recommended_for_you,
|
||||
),
|
||||
SectionHeader(title: sectionsI18n.recommended_for_you),
|
||||
BlocBuilder<HomeCubit, HomeState>(
|
||||
builder: (context, state) {
|
||||
if (state.recommendedShifts.isEmpty) {
|
||||
@@ -201,7 +204,8 @@ class WorkerHomePage extends StatelessWidget {
|
||||
clipBehavior: Clip.none,
|
||||
itemBuilder: (context, index) => Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: UiConstants.space3),
|
||||
right: UiConstants.space3,
|
||||
),
|
||||
child: RecommendedShiftCard(
|
||||
shift: state.recommendedShifts[index],
|
||||
),
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
import 'package:design_system/design_system.dart';
|
||||
|
||||
|
||||
/// Banner widget for placeholder actions, using design system tokens.
|
||||
class PlaceholderBanner extends StatelessWidget {
|
||||
/// Banner title
|
||||
final String title;
|
||||
|
||||
/// Banner subtitle
|
||||
final String subtitle;
|
||||
|
||||
/// Banner background color
|
||||
final Color bg;
|
||||
|
||||
/// Banner accent color
|
||||
final Color accent;
|
||||
|
||||
/// Optional tap callback
|
||||
final VoidCallback? onTap;
|
||||
|
||||
/// Creates a [PlaceholderBanner].
|
||||
const PlaceholderBanner({super.key, required this.title, required this.subtitle, required this.bg, required this.accent, this.onTap});
|
||||
const PlaceholderBanner({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.bg,
|
||||
required this.accent,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -29,7 +38,7 @@ class PlaceholderBanner extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: accent.withValues(alpha: 0.3)),
|
||||
border: Border.all(color: accent, width: 1),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -41,7 +50,11 @@ class PlaceholderBanner extends StatelessWidget {
|
||||
color: UiColors.bgBanner,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(UiIcons.sparkles, color: accent, size: UiConstants.space5),
|
||||
child: Icon(
|
||||
UiIcons.sparkles,
|
||||
color: accent,
|
||||
size: UiConstants.space5,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
@@ -50,12 +63,9 @@ class PlaceholderBanner extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: UiTypography.body1b,
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: UiTypography.body3r.copyWith(color: UiColors.mutedForeground),
|
||||
style: UiTypography.body1b.copyWith(color: accent),
|
||||
),
|
||||
Text(subtitle, style: UiTypography.body3r.textSecondary),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -8,14 +8,11 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import '../blocs/profile_cubit.dart';
|
||||
import '../blocs/profile_state.dart';
|
||||
import '../widgets/language_selector_bottom_sheet.dart';
|
||||
import '../widgets/logout_button.dart';
|
||||
import '../widgets/profile_header.dart';
|
||||
import '../widgets/profile_menu_grid.dart';
|
||||
import '../widgets/profile_menu_item.dart';
|
||||
import '../widgets/reliability_score_bar.dart';
|
||||
import '../widgets/reliability_stats_card.dart';
|
||||
import '../widgets/section_title.dart';
|
||||
import '../widgets/sections/index.dart';
|
||||
|
||||
/// The main Staff Profile page.
|
||||
///
|
||||
@@ -49,7 +46,6 @@ class StaffProfilePage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = Translations.of(context).staff.profile;
|
||||
final ProfileCubit cubit = Modular.get<ProfileCubit>();
|
||||
|
||||
// Load profile data on first build
|
||||
@@ -60,7 +56,7 @@ class StaffProfilePage extends StatelessWidget {
|
||||
return Scaffold(
|
||||
body: BlocConsumer<ProfileCubit, ProfileState>(
|
||||
bloc: cubit,
|
||||
listener: (context, state) {
|
||||
listener: (BuildContext context, ProfileState state) {
|
||||
if (state.status == ProfileStatus.signedOut) {
|
||||
Modular.to.toGetStartedPage();
|
||||
} else if (state.status == ProfileStatus.error &&
|
||||
@@ -72,7 +68,7 @@ class StaffProfilePage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
builder: (BuildContext context, ProfileState state) {
|
||||
// Show loading spinner if status is loading
|
||||
if (state.status == ProfileStatus.loading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
@@ -95,7 +91,7 @@ class StaffProfilePage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
final profile = state.profile;
|
||||
final Staff? profile = state.profile;
|
||||
if (profile == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
@@ -103,7 +99,7 @@ class StaffProfilePage extends StatelessWidget {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(bottom: UiConstants.space16),
|
||||
child: Column(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
ProfileHeader(
|
||||
fullName: profile.name,
|
||||
level: _mapStatusToLevel(profile.status),
|
||||
@@ -117,7 +113,7 @@ class StaffProfilePage extends StatelessWidget {
|
||||
horizontal: UiConstants.space5,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
ReliabilityStatsCard(
|
||||
totalShifts: profile.totalShifts,
|
||||
averageRating: profile.averageRating,
|
||||
@@ -130,100 +126,15 @@ class StaffProfilePage extends StatelessWidget {
|
||||
reliabilityScore: profile.reliabilityScore,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
SectionTitle(i18n.sections.onboarding),
|
||||
ProfileMenuGrid(
|
||||
crossAxisCount: 3,
|
||||
|
||||
children: [
|
||||
ProfileMenuItem(
|
||||
icon: UiIcons.user,
|
||||
label: i18n.menu_items.personal_info,
|
||||
onTap: () => Modular.to.toPersonalInfo(),
|
||||
),
|
||||
ProfileMenuItem(
|
||||
icon: UiIcons.phone,
|
||||
label: i18n.menu_items.emergency_contact,
|
||||
onTap: () => Modular.to.toEmergencyContact(),
|
||||
),
|
||||
ProfileMenuItem(
|
||||
icon: UiIcons.briefcase,
|
||||
label: i18n.menu_items.experience,
|
||||
onTap: () => Modular.to.toExperience(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const OnboardingSection(),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SectionTitle(i18n.sections.compliance),
|
||||
ProfileMenuGrid(
|
||||
crossAxisCount: 3,
|
||||
children: [
|
||||
ProfileMenuItem(
|
||||
icon: UiIcons.file,
|
||||
label: i18n.menu_items.tax_forms,
|
||||
onTap: () => Modular.to.toTaxForms(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const ComplianceSection(),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
SectionTitle(i18n.sections.finance),
|
||||
ProfileMenuGrid(
|
||||
crossAxisCount: 3,
|
||||
children: [
|
||||
ProfileMenuItem(
|
||||
icon: UiIcons.building,
|
||||
label: i18n.menu_items.bank_account,
|
||||
onTap: () => Modular.to.toBankAccount(),
|
||||
),
|
||||
ProfileMenuItem(
|
||||
icon: UiIcons.creditCard,
|
||||
label: i18n.menu_items.payments,
|
||||
onTap: () => Modular.to.toPayments(),
|
||||
),
|
||||
ProfileMenuItem(
|
||||
icon: UiIcons.clock,
|
||||
label: i18n.menu_items.timecard,
|
||||
onTap: () => Modular.to.toTimeCard(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const FinanceSection(),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
SectionTitle(
|
||||
i18n.header.title.contains("Perfil") ? "Soporte" : "Support",
|
||||
),
|
||||
ProfileMenuGrid(
|
||||
crossAxisCount: 3,
|
||||
children: [
|
||||
ProfileMenuItem(
|
||||
icon: UiIcons.shield,
|
||||
label: i18n.header.title.contains("Perfil") ? "Privacidad" : "Privacy & Security",
|
||||
onTap: () => Modular.to.toPrivacySecurity(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SupportSection(),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
SectionTitle(
|
||||
i18n.header.title.contains("Perfil") ? "Ajustes" : "Settings",
|
||||
),
|
||||
ProfileMenuGrid(
|
||||
crossAxisCount: 3,
|
||||
children: [
|
||||
ProfileMenuItem(
|
||||
icon: UiIcons.globe,
|
||||
label: i18n.header.title.contains("Perfil") ? "Idioma" : "Language",
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => const LanguageSelectorBottomSheet(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SettingsSection(),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
LogoutButton(
|
||||
onTap: () => _onSignOut(cubit, state),
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
import '../profile_menu_grid.dart';
|
||||
import '../profile_menu_item.dart';
|
||||
import '../section_title.dart';
|
||||
|
||||
/// Widget displaying the compliance section of the staff profile.
|
||||
///
|
||||
/// This section contains menu items for tax forms and other compliance-related documents.
|
||||
class ComplianceSection extends StatelessWidget {
|
||||
/// Creates a [ComplianceSection].
|
||||
const ComplianceSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TranslationsStaffProfileEn i18n = Translations.of(context).staff.profile;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
SectionTitle(i18n.sections.compliance),
|
||||
ProfileMenuGrid(
|
||||
crossAxisCount: 3,
|
||||
children: <Widget>[
|
||||
ProfileMenuItem(
|
||||
icon: UiIcons.file,
|
||||
label: i18n.menu_items.tax_forms,
|
||||
onTap: () => Modular.to.toTaxForms(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
import '../profile_menu_grid.dart';
|
||||
import '../profile_menu_item.dart';
|
||||
import '../section_title.dart';
|
||||
|
||||
/// Widget displaying the finance section of the staff profile.
|
||||
///
|
||||
/// This section contains menu items for bank account, payments, and timecard information.
|
||||
class FinanceSection extends StatelessWidget {
|
||||
/// Creates a [FinanceSection].
|
||||
const FinanceSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TranslationsStaffProfileEn i18n = Translations.of(context).staff.profile;
|
||||
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
SectionTitle(i18n.sections.finance),
|
||||
ProfileMenuGrid(
|
||||
crossAxisCount: 3,
|
||||
children: <Widget>[
|
||||
ProfileMenuItem(
|
||||
icon: UiIcons.building,
|
||||
label: i18n.menu_items.bank_account,
|
||||
onTap: () => Modular.to.toBankAccount(),
|
||||
),
|
||||
ProfileMenuItem(
|
||||
icon: UiIcons.creditCard,
|
||||
label: i18n.menu_items.payments,
|
||||
onTap: () => Modular.to.toPayments(),
|
||||
),
|
||||
ProfileMenuItem(
|
||||
icon: UiIcons.clock,
|
||||
label: i18n.menu_items.timecard,
|
||||
onTap: () => Modular.to.toTimeCard(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export 'compliance_section.dart';
|
||||
export 'finance_section.dart';
|
||||
export 'onboarding_section.dart';
|
||||
export 'settings_section.dart';
|
||||
export 'support_section.dart';
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
import '../profile_menu_grid.dart';
|
||||
import '../profile_menu_item.dart';
|
||||
import '../section_title.dart';
|
||||
|
||||
/// Widget displaying the onboarding section of the staff profile.
|
||||
///
|
||||
/// This section contains menu items for personal information, emergency contact,
|
||||
/// and work experience setup.
|
||||
class OnboardingSection extends StatelessWidget {
|
||||
/// Creates an [OnboardingSection].
|
||||
const OnboardingSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TranslationsStaffProfileEn i18n = Translations.of(context).staff.profile;
|
||||
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
SectionTitle(i18n.sections.onboarding),
|
||||
ProfileMenuGrid(
|
||||
crossAxisCount: 3,
|
||||
children: <Widget>[
|
||||
ProfileMenuItem(
|
||||
icon: UiIcons.user,
|
||||
label: i18n.menu_items.personal_info,
|
||||
onTap: () => Modular.to.toPersonalInfo(),
|
||||
),
|
||||
ProfileMenuItem(
|
||||
icon: UiIcons.phone,
|
||||
label: i18n.menu_items.emergency_contact,
|
||||
onTap: () => Modular.to.toEmergencyContact(),
|
||||
),
|
||||
ProfileMenuItem(
|
||||
icon: UiIcons.briefcase,
|
||||
label: i18n.menu_items.experience,
|
||||
onTap: () => Modular.to.toExperience(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../language_selector_bottom_sheet.dart';
|
||||
import '../profile_menu_grid.dart';
|
||||
import '../profile_menu_item.dart';
|
||||
import '../section_title.dart';
|
||||
|
||||
/// Widget displaying the settings section of the staff profile.
|
||||
///
|
||||
/// This section contains menu items for language selection.
|
||||
class SettingsSection extends StatelessWidget {
|
||||
/// Creates a [SettingsSection].
|
||||
const SettingsSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TranslationsStaffProfileEn i18n = Translations.of(
|
||||
context,
|
||||
).staff.profile;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
||||
children: <Widget>[
|
||||
SectionTitle(i18n.sections.settings),
|
||||
ProfileMenuGrid(
|
||||
crossAxisCount: 3,
|
||||
children: <Widget>[
|
||||
ProfileMenuItem(
|
||||
icon: UiIcons.globe,
|
||||
label: i18n.menu_items.language,
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (BuildContext context) =>
|
||||
const LanguageSelectorBottomSheet(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
import '../profile_menu_grid.dart';
|
||||
import '../profile_menu_item.dart';
|
||||
import '../section_title.dart';
|
||||
|
||||
/// Widget displaying the support section of the staff profile.
|
||||
///
|
||||
/// This section contains menu items for FAQs and privacy & security settings.
|
||||
class SupportSection extends StatelessWidget {
|
||||
/// Creates a [SupportSection].
|
||||
const SupportSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TranslationsStaffProfileEn i18n = Translations.of(
|
||||
context,
|
||||
).staff.profile;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
SectionTitle(i18n.sections.support),
|
||||
ProfileMenuGrid(
|
||||
crossAxisCount: 3,
|
||||
children: <Widget>[
|
||||
ProfileMenuItem(
|
||||
icon: UiIcons.helpCircle,
|
||||
label: i18n.menu_items.faqs,
|
||||
onTap: () => Modular.to.toFaqs(),
|
||||
),
|
||||
ProfileMenuItem(
|
||||
icon: UiIcons.shield,
|
||||
label: i18n.menu_items.privacy_security,
|
||||
onTap: () => Modular.to.toPrivacySecurity(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
[
|
||||
{
|
||||
"category": "Getting Started",
|
||||
"questions": [
|
||||
{
|
||||
"q": "How do I apply for shifts?",
|
||||
"a": "Browse available shifts on the Shifts tab and tap \"Accept\" on any shift that interests you. Once confirmed, you'll receive all the details you need."
|
||||
},
|
||||
{
|
||||
"q": "How do I get paid?",
|
||||
"a": "Payments are processed weekly via direct deposit to your linked bank account. You can view your earnings in the Payments section."
|
||||
},
|
||||
{
|
||||
"q": "What if I need to cancel a shift?",
|
||||
"a": "You can cancel a shift up to 24 hours before it starts without penalty. Late cancellations may affect your reliability score."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "Shifts & Work",
|
||||
"questions": [
|
||||
{
|
||||
"q": "How do I clock in?",
|
||||
"a": "Use the Clock In feature on the home screen when you arrive at your shift. Make sure location services are enabled for verification."
|
||||
},
|
||||
{
|
||||
"q": "What should I wear?",
|
||||
"a": "Check the shift details for dress code requirements. You can manage your wardrobe in the Attire section of your profile."
|
||||
},
|
||||
{
|
||||
"q": "Who do I contact if I'm running late?",
|
||||
"a": "Use the \"Running Late\" feature in the app to notify the client. You can also message the shift manager directly."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "Payments & Earnings",
|
||||
"questions": [
|
||||
{
|
||||
"q": "When do I get paid?",
|
||||
"a": "Payments are processed every Friday for shifts completed the previous week. Funds typically arrive within 1-2 business days."
|
||||
},
|
||||
{
|
||||
"q": "How do I update my bank account?",
|
||||
"a": "Go to Profile > Finance > Bank Account to add or update your banking information."
|
||||
},
|
||||
{
|
||||
"q": "Where can I find my tax documents?",
|
||||
"a": "Tax documents (1099) are available in Profile > Compliance > Tax Documents by January 31st each year."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,101 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../../domain/entities/faq_category.dart';
|
||||
import '../../domain/entities/faq_item.dart';
|
||||
import '../../domain/repositories/faqs_repository_interface.dart';
|
||||
|
||||
/// Data layer implementation of FAQs repository
|
||||
///
|
||||
/// Handles loading FAQs from app assets (JSON file)
|
||||
class FaqsRepositoryImpl implements FaqsRepositoryInterface {
|
||||
/// Private cache for FAQs to avoid reloading from assets multiple times
|
||||
List<FaqCategory>? _cachedFaqs;
|
||||
|
||||
@override
|
||||
Future<List<FaqCategory>> getFaqs() async {
|
||||
try {
|
||||
// Return cached FAQs if available
|
||||
if (_cachedFaqs != null) {
|
||||
return _cachedFaqs!;
|
||||
}
|
||||
|
||||
// Load FAQs from JSON asset
|
||||
final String faqsJson = await rootBundle.loadString(
|
||||
'packages/staff_faqs/lib/src/assets/faqs/faqs.json',
|
||||
);
|
||||
|
||||
// Parse JSON
|
||||
final List<dynamic> decoded = jsonDecode(faqsJson) as List<dynamic>;
|
||||
|
||||
// Convert to domain entities
|
||||
_cachedFaqs = decoded.map((dynamic item) {
|
||||
final Map<String, dynamic> category = item as Map<String, dynamic>;
|
||||
final String categoryName = category['category'] as String;
|
||||
final List<dynamic> questionsData =
|
||||
category['questions'] as List<dynamic>;
|
||||
|
||||
final List<FaqItem> questions = questionsData.map((dynamic q) {
|
||||
final Map<String, dynamic> questionMap = q as Map<String, dynamic>;
|
||||
return FaqItem(
|
||||
question: questionMap['q'] as String,
|
||||
answer: questionMap['a'] as String,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return FaqCategory(
|
||||
category: categoryName,
|
||||
questions: questions,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return _cachedFaqs!;
|
||||
} catch (e) {
|
||||
// Return empty list on error
|
||||
return <FaqCategory>[];
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<FaqCategory>> searchFaqs(String query) async {
|
||||
try {
|
||||
// Get all FAQs first
|
||||
final List<FaqCategory> allFaqs = await getFaqs();
|
||||
|
||||
if (query.isEmpty) {
|
||||
return allFaqs;
|
||||
}
|
||||
|
||||
final String lowerQuery = query.toLowerCase();
|
||||
|
||||
// Filter categories based on matching questions
|
||||
final List<FaqCategory> filtered = allFaqs
|
||||
.map((FaqCategory category) {
|
||||
// Filter questions that match the query
|
||||
final List<FaqItem> matchingQuestions =
|
||||
category.questions.where((FaqItem item) {
|
||||
final String questionLower = item.question.toLowerCase();
|
||||
final String answerLower = item.answer.toLowerCase();
|
||||
return questionLower.contains(lowerQuery) ||
|
||||
answerLower.contains(lowerQuery);
|
||||
}).toList();
|
||||
|
||||
// Only include category if it has matching questions
|
||||
if (matchingQuestions.isNotEmpty) {
|
||||
return FaqCategory(
|
||||
category: category.category,
|
||||
questions: matchingQuestions,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.whereType<FaqCategory>()
|
||||
.toList();
|
||||
|
||||
return filtered;
|
||||
} catch (e) {
|
||||
return <FaqCategory>[];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import 'faq_item.dart';
|
||||
|
||||
/// Entity representing an FAQ category with its questions
|
||||
class FaqCategory extends Equatable {
|
||||
/// The category name (e.g., "Getting Started", "Shifts & Work")
|
||||
final String category;
|
||||
|
||||
/// List of FAQ items in this category
|
||||
final List<FaqItem> questions;
|
||||
|
||||
const FaqCategory({
|
||||
required this.category,
|
||||
required this.questions,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[category, questions];
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Entity representing a single FAQ question and answer
|
||||
class FaqItem extends Equatable {
|
||||
/// The question text
|
||||
final String question;
|
||||
|
||||
/// The answer text
|
||||
final String answer;
|
||||
|
||||
const FaqItem({
|
||||
required this.question,
|
||||
required this.answer,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[question, answer];
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import '../entities/faq_category.dart';
|
||||
|
||||
/// Interface for FAQs repository operations
|
||||
abstract class FaqsRepositoryInterface {
|
||||
/// Fetch all FAQ categories with their questions
|
||||
Future<List<FaqCategory>> getFaqs();
|
||||
|
||||
/// Search FAQs by query string
|
||||
/// Returns categories that contain matching questions
|
||||
Future<List<FaqCategory>> searchFaqs(String query);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import '../entities/faq_category.dart';
|
||||
import '../repositories/faqs_repository_interface.dart';
|
||||
|
||||
/// Use case to retrieve all FAQs
|
||||
class GetFaqsUseCase {
|
||||
final FaqsRepositoryInterface _repository;
|
||||
|
||||
GetFaqsUseCase(this._repository);
|
||||
|
||||
/// Execute the use case to get all FAQ categories
|
||||
Future<List<FaqCategory>> call() async {
|
||||
try {
|
||||
return await _repository.getFaqs();
|
||||
} catch (e) {
|
||||
// Return empty list on error
|
||||
return <FaqCategory>[];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import '../entities/faq_category.dart';
|
||||
import '../repositories/faqs_repository_interface.dart';
|
||||
|
||||
/// Parameters for search FAQs use case
|
||||
class SearchFaqsParams {
|
||||
/// Search query string
|
||||
final String query;
|
||||
|
||||
SearchFaqsParams({required this.query});
|
||||
}
|
||||
|
||||
/// Use case to search FAQs by query
|
||||
class SearchFaqsUseCase {
|
||||
final FaqsRepositoryInterface _repository;
|
||||
|
||||
SearchFaqsUseCase(this._repository);
|
||||
|
||||
/// Execute the use case to search FAQs
|
||||
Future<List<FaqCategory>> call(SearchFaqsParams params) async {
|
||||
try {
|
||||
return await _repository.searchFaqs(params.query);
|
||||
} catch (e) {
|
||||
// Return empty list on error
|
||||
return <FaqCategory>[];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import '../../domain/entities/faq_category.dart';
|
||||
import '../../domain/usecases/get_faqs_usecase.dart';
|
||||
import '../../domain/usecases/search_faqs_usecase.dart';
|
||||
|
||||
part 'faqs_event.dart';
|
||||
part 'faqs_state.dart';
|
||||
|
||||
/// BLoC managing FAQs state
|
||||
class FaqsBloc extends Bloc<FaqsEvent, FaqsState> {
|
||||
final GetFaqsUseCase _getFaqsUseCase;
|
||||
final SearchFaqsUseCase _searchFaqsUseCase;
|
||||
|
||||
FaqsBloc({
|
||||
required GetFaqsUseCase getFaqsUseCase,
|
||||
required SearchFaqsUseCase searchFaqsUseCase,
|
||||
}) : _getFaqsUseCase = getFaqsUseCase,
|
||||
_searchFaqsUseCase = searchFaqsUseCase,
|
||||
super(const FaqsState()) {
|
||||
on<FetchFaqsEvent>(_onFetchFaqs);
|
||||
on<SearchFaqsEvent>(_onSearchFaqs);
|
||||
}
|
||||
|
||||
Future<void> _onFetchFaqs(
|
||||
FetchFaqsEvent event,
|
||||
Emitter<FaqsState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(isLoading: true, error: null));
|
||||
|
||||
try {
|
||||
final List<FaqCategory> categories = await _getFaqsUseCase.call();
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
categories: categories,
|
||||
searchQuery: '',
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
error: 'Failed to load FAQs',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSearchFaqs(
|
||||
SearchFaqsEvent event,
|
||||
Emitter<FaqsState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(isLoading: true, error: null, searchQuery: event.query));
|
||||
|
||||
try {
|
||||
final List<FaqCategory> results = await _searchFaqsUseCase.call(
|
||||
SearchFaqsParams(query: event.query),
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
categories: results,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
error: 'Failed to search FAQs',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
part of 'faqs_bloc.dart';
|
||||
|
||||
/// Base class for FAQs BLoC events
|
||||
abstract class FaqsEvent extends Equatable {
|
||||
const FaqsEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[];
|
||||
}
|
||||
|
||||
/// Event to fetch all FAQs
|
||||
class FetchFaqsEvent extends FaqsEvent {
|
||||
const FetchFaqsEvent();
|
||||
}
|
||||
|
||||
/// Event to search FAQs by query
|
||||
class SearchFaqsEvent extends FaqsEvent {
|
||||
/// Search query string
|
||||
final String query;
|
||||
|
||||
const SearchFaqsEvent({required this.query});
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[query];
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
part of 'faqs_bloc.dart';
|
||||
|
||||
/// State for FAQs BLoC
|
||||
class FaqsState extends Equatable {
|
||||
/// List of FAQ categories currently displayed
|
||||
final List<FaqCategory> categories;
|
||||
|
||||
/// Whether FAQs are currently loading
|
||||
final bool isLoading;
|
||||
|
||||
/// Current search query
|
||||
final String searchQuery;
|
||||
|
||||
/// Error message, if any
|
||||
final String? error;
|
||||
|
||||
const FaqsState({
|
||||
this.categories = const <FaqCategory>[],
|
||||
this.isLoading = false,
|
||||
this.searchQuery = '',
|
||||
this.error,
|
||||
});
|
||||
|
||||
/// Create a copy with optional field overrides
|
||||
FaqsState copyWith({
|
||||
List<FaqCategory>? categories,
|
||||
bool? isLoading,
|
||||
String? searchQuery,
|
||||
String? error,
|
||||
}) {
|
||||
return FaqsState(
|
||||
categories: categories ?? this.categories,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
searchQuery: searchQuery ?? this.searchQuery,
|
||||
error: error,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
categories,
|
||||
isLoading,
|
||||
searchQuery,
|
||||
error,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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:flutter_modular/flutter_modular.dart';
|
||||
|
||||
import '../blocs/faqs_bloc.dart';
|
||||
import '../widgets/faqs_widget.dart';
|
||||
|
||||
/// Page displaying frequently asked questions
|
||||
class FaqsPage extends StatelessWidget {
|
||||
const FaqsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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()]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
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:staff_faqs/src/presentation/blocs/faqs_bloc.dart';
|
||||
|
||||
/// Widget displaying FAQs with search functionality and accordion items
|
||||
class FaqsWidget extends StatefulWidget {
|
||||
const FaqsWidget({super.key});
|
||||
|
||||
@override
|
||||
State<FaqsWidget> createState() => _FaqsWidgetState();
|
||||
}
|
||||
|
||||
class _FaqsWidgetState extends State<FaqsWidget> {
|
||||
late TextEditingController _searchController;
|
||||
final Map<String, bool> _openItems = <String, bool>{};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController = TextEditingController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _toggleItem(String key) {
|
||||
setState(() {
|
||||
_openItems[key] = !(_openItems[key] ?? false);
|
||||
});
|
||||
}
|
||||
|
||||
void _onSearchChanged(String value) {
|
||||
if (value.isEmpty) {
|
||||
context.read<FaqsBloc>().add(const FetchFaqsEvent());
|
||||
} else {
|
||||
context.read<FaqsBloc>().add(SearchFaqsEvent(query: value));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<FaqsBloc, FaqsState>(
|
||||
builder: (BuildContext context, FaqsState state) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 100),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
// Search Bar
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
onChanged: _onSearchChanged,
|
||||
decoration: InputDecoration(
|
||||
hintText: t.staff_faqs.search_placeholder,
|
||||
hintStyle: const TextStyle(color: UiColors.textPlaceholder),
|
||||
prefixIcon: const Icon(
|
||||
UiIcons.search,
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// FAQ List or Empty State
|
||||
if (state.isLoading)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 48),
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
else if (state.categories.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 48),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.helpCircle,
|
||||
size: 48,
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
t.staff_faqs.no_results,
|
||||
style: const TextStyle(color: UiColors.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
...state.categories.asMap().entries.map((
|
||||
MapEntry<int, dynamic> entry,
|
||||
) {
|
||||
final int catIndex = entry.key;
|
||||
final dynamic categoryItem = entry.value;
|
||||
final String categoryName = categoryItem.category;
|
||||
final List<dynamic> questions = categoryItem.questions;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
categoryName,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
...questions.asMap().entries.map((
|
||||
MapEntry<int, dynamic> qEntry,
|
||||
) {
|
||||
final int qIndex = qEntry.key;
|
||||
final dynamic questionItem = qEntry.value;
|
||||
final String key = '$catIndex-$qIndex';
|
||||
final bool isOpen = _openItems[key] ?? false;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(
|
||||
UiConstants.radiusBase,
|
||||
),
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
InkWell(
|
||||
onTap: () => _toggleItem(key),
|
||||
borderRadius: BorderRadius.circular(
|
||||
UiConstants.radiusBase,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
questionItem.question,
|
||||
style: UiTypography.body1r,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
isOpen
|
||||
? UiIcons.chevronUp
|
||||
: UiIcons.chevronDown,
|
||||
color: UiColors.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isOpen)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
16,
|
||||
0,
|
||||
16,
|
||||
16,
|
||||
),
|
||||
child: Text(
|
||||
questionItem.answer,
|
||||
style: UiTypography.body1r.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
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';
|
||||
import 'domain/usecases/get_faqs_usecase.dart';
|
||||
import 'domain/usecases/search_faqs_usecase.dart';
|
||||
import 'presentation/blocs/faqs_bloc.dart';
|
||||
import 'presentation/pages/faqs_page.dart';
|
||||
|
||||
/// Module for FAQs feature
|
||||
///
|
||||
/// Provides:
|
||||
/// - Dependency injection for repositories, use cases, and BLoCs
|
||||
/// - Route definitions delegated to core routing
|
||||
class FaqsModule extends Module {
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
// Repository
|
||||
i.addSingleton<FaqsRepositoryInterface>(
|
||||
() => FaqsRepositoryImpl(),
|
||||
);
|
||||
|
||||
// Use Cases
|
||||
i.addSingleton(
|
||||
() => GetFaqsUseCase(
|
||||
i<FaqsRepositoryInterface>(),
|
||||
),
|
||||
);
|
||||
i.addSingleton(
|
||||
() => SearchFaqsUseCase(
|
||||
i<FaqsRepositoryInterface>(),
|
||||
),
|
||||
);
|
||||
|
||||
// BLoC
|
||||
i.add(
|
||||
() => FaqsBloc(
|
||||
getFaqsUseCase: i<GetFaqsUseCase>(),
|
||||
searchFaqsUseCase: i<SearchFaqsUseCase>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void routes(RouteManager r) {
|
||||
r.child(
|
||||
StaffPaths.childRoute(StaffPaths.faqs, StaffPaths.faqs),
|
||||
child: (_) => const FaqsPage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
library staff_faqs;
|
||||
|
||||
export 'src/staff_faqs_module.dart';
|
||||
export 'src/presentation/pages/faqs_page.dart';
|
||||
@@ -0,0 +1,29 @@
|
||||
name: staff_faqs
|
||||
description: Frequently Asked Questions feature for staff application.
|
||||
version: 0.0.1
|
||||
publish_to: none
|
||||
resolution: workspace
|
||||
|
||||
environment:
|
||||
sdk: '>=3.10.0 <4.0.0'
|
||||
flutter: ">=3.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_bloc: ^8.1.0
|
||||
flutter_modular: ^6.3.0
|
||||
equatable: ^2.0.5
|
||||
|
||||
# Architecture Packages
|
||||
krow_core:
|
||||
path: ../../../../../core
|
||||
design_system:
|
||||
path: ../../../../../design_system
|
||||
core_localization:
|
||||
path: ../../../../../core_localization
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
assets:
|
||||
- lib/src/assets/faqs/
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,9 @@ dependencies:
|
||||
staff_clock_in:
|
||||
path: ../clock_in
|
||||
staff_privacy_security:
|
||||
path: ../profile_sections/settings/privacy_security
|
||||
path: ../profile_sections/support/privacy_security
|
||||
staff_faqs:
|
||||
path: ../profile_sections/support/faqs
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -1290,10 +1290,17 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.1"
|
||||
staff_faqs:
|
||||
dependency: transitive
|
||||
description:
|
||||
path: "packages/features/staff/profile_sections/support/faqs"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
staff_privacy_security:
|
||||
dependency: transitive
|
||||
description:
|
||||
path: "packages/features/staff/profile_sections/settings/privacy_security"
|
||||
path: "packages/features/staff/profile_sections/support/privacy_security"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
|
||||
@@ -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