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.
|
- **[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.
|
- **[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
|
## 🤝 Contributing
|
||||||
New to the team? Please read our **[Contributing Guide](./docs/03-contributing.md)** to get your environment set up and understand our workflow.
|
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';
|
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
|
||||||
|
|||||||
@@ -521,7 +521,8 @@
|
|||||||
"compliance": "COMPLIANCE",
|
"compliance": "COMPLIANCE",
|
||||||
"level_up": "LEVEL UP",
|
"level_up": "LEVEL UP",
|
||||||
"finance": "FINANCE",
|
"finance": "FINANCE",
|
||||||
"support": "SUPPORT"
|
"support": "SUPPORT",
|
||||||
|
"settings": "SETTINGS"
|
||||||
},
|
},
|
||||||
"menu_items": {
|
"menu_items": {
|
||||||
"personal_info": "Personal Info",
|
"personal_info": "Personal Info",
|
||||||
@@ -543,7 +544,8 @@
|
|||||||
"timecard": "Timecard",
|
"timecard": "Timecard",
|
||||||
"faqs": "FAQs",
|
"faqs": "FAQs",
|
||||||
"privacy_security": "Privacy & Security",
|
"privacy_security": "Privacy & Security",
|
||||||
"messages": "Messages"
|
"messages": "Messages",
|
||||||
|
"language": "Language"
|
||||||
},
|
},
|
||||||
"bank_account_page": {
|
"bank_account_page": {
|
||||||
"title": "Bank Account",
|
"title": "Bank Account",
|
||||||
@@ -1143,6 +1145,12 @@
|
|||||||
"profile_visibility_updated": "Profile visibility updated successfully!"
|
"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": {
|
"success": {
|
||||||
"hub": {
|
"hub": {
|
||||||
"created": "Hub created successfully!",
|
"created": "Hub created successfully!",
|
||||||
|
|||||||
@@ -521,7 +521,8 @@
|
|||||||
"compliance": "CUMPLIMIENTO",
|
"compliance": "CUMPLIMIENTO",
|
||||||
"level_up": "MEJORAR NIVEL",
|
"level_up": "MEJORAR NIVEL",
|
||||||
"finance": "FINANZAS",
|
"finance": "FINANZAS",
|
||||||
"support": "SOPORTE"
|
"support": "SOPORTE",
|
||||||
|
"settings": "AJUSTES"
|
||||||
},
|
},
|
||||||
"menu_items": {
|
"menu_items": {
|
||||||
"personal_info": "Información Personal",
|
"personal_info": "Información Personal",
|
||||||
@@ -543,7 +544,8 @@
|
|||||||
"timecard": "Tarjeta de Tiempo",
|
"timecard": "Tarjeta de Tiempo",
|
||||||
"faqs": "Preguntas Frecuentes",
|
"faqs": "Preguntas Frecuentes",
|
||||||
"privacy_security": "Privacidad y Seguridad",
|
"privacy_security": "Privacidad y Seguridad",
|
||||||
"messages": "Mensajes"
|
"messages": "Mensajes",
|
||||||
|
"language": "Idioma"
|
||||||
},
|
},
|
||||||
"bank_account_page": {
|
"bank_account_page": {
|
||||||
"title": "Cuenta Bancaria",
|
"title": "Cuenta Bancaria",
|
||||||
@@ -1143,6 +1145,12 @@
|
|||||||
"profile_visibility_updated": "¡Visibilidad del perfil actualizada exitosamente!"
|
"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": {
|
"success": {
|
||||||
"hub": {
|
"hub": {
|
||||||
"created": "¡Hub creado exitosamente!",
|
"created": "¡Hub creado exitosamente!",
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ class UiColors {
|
|||||||
/// Foreground color on primary background (#F7FAFC)
|
/// Foreground color on primary background (#F7FAFC)
|
||||||
static const Color primaryForeground = Color(0xFFF7FAFC);
|
static const Color primaryForeground = Color(0xFFF7FAFC);
|
||||||
|
|
||||||
/// Inverse primary color (#9FABF1)
|
/// Inverse primary color (#0A39DF)
|
||||||
static const Color primaryInverse = Color(0xFF9FABF1);
|
static const Color primaryInverse = Color.fromARGB(23, 10, 56, 223);
|
||||||
|
|
||||||
/// Secondary background color (#F1F3F5)
|
/// Secondary background color (#F1F3F5)
|
||||||
static const Color secondary = Color(0xFFF1F3F5);
|
static const Color secondary = Color(0xFFF1F3F5);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,13 +49,17 @@ class WorkerHomePage extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
BlocBuilder<HomeCubit, HomeState>(
|
BlocBuilder<HomeCubit, HomeState>(
|
||||||
buildWhen: (previous, current) => previous.staffName != current.staffName,
|
buildWhen: (previous, current) =>
|
||||||
|
previous.staffName != current.staffName,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return HomeHeader(userName: state.staffName);
|
return HomeHeader(userName: state.staffName);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space4),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: UiConstants.space4,
|
||||||
|
vertical: UiConstants.space4,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
BlocBuilder<HomeCubit, HomeState>(
|
BlocBuilder<HomeCubit, HomeState>(
|
||||||
@@ -67,7 +71,7 @@ class WorkerHomePage extends StatelessWidget {
|
|||||||
return PlaceholderBanner(
|
return PlaceholderBanner(
|
||||||
title: bannersI18n.complete_profile_title,
|
title: bannersI18n.complete_profile_title,
|
||||||
subtitle: bannersI18n.complete_profile_subtitle,
|
subtitle: bannersI18n.complete_profile_subtitle,
|
||||||
bg: UiColors.bgHighlight,
|
bg: UiColors.primaryInverse,
|
||||||
accent: UiColors.primary,
|
accent: UiColors.primary,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Modular.to.toProfile();
|
Modular.to.toProfile();
|
||||||
@@ -135,7 +139,8 @@ class WorkerHomePage extends StatelessWidget {
|
|||||||
EmptyStateWidget(
|
EmptyStateWidget(
|
||||||
message: emptyI18n.no_shifts_today,
|
message: emptyI18n.no_shifts_today,
|
||||||
actionLink: emptyI18n.find_shifts_cta,
|
actionLink: emptyI18n.find_shifts_cta,
|
||||||
onAction: () => Modular.to.toShifts(initialTab: 'find'),
|
onAction: () =>
|
||||||
|
Modular.to.toShifts(initialTab: 'find'),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
Column(
|
Column(
|
||||||
@@ -183,9 +188,7 @@ class WorkerHomePage extends StatelessWidget {
|
|||||||
const SizedBox(height: UiConstants.space6),
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
|
||||||
// Recommended Shifts
|
// Recommended Shifts
|
||||||
SectionHeader(
|
SectionHeader(title: sectionsI18n.recommended_for_you),
|
||||||
title: sectionsI18n.recommended_for_you,
|
|
||||||
),
|
|
||||||
BlocBuilder<HomeCubit, HomeState>(
|
BlocBuilder<HomeCubit, HomeState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.recommendedShifts.isEmpty) {
|
if (state.recommendedShifts.isEmpty) {
|
||||||
@@ -201,7 +204,8 @@ class WorkerHomePage extends StatelessWidget {
|
|||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
itemBuilder: (context, index) => Padding(
|
itemBuilder: (context, index) => Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
right: UiConstants.space3),
|
right: UiConstants.space3,
|
||||||
|
),
|
||||||
child: RecommendedShiftCard(
|
child: RecommendedShiftCard(
|
||||||
shift: state.recommendedShifts[index],
|
shift: state.recommendedShifts[index],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,24 +1,33 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
|
|
||||||
|
|
||||||
/// Banner widget for placeholder actions, using design system tokens.
|
/// Banner widget for placeholder actions, using design system tokens.
|
||||||
class PlaceholderBanner extends StatelessWidget {
|
class PlaceholderBanner extends StatelessWidget {
|
||||||
/// Banner title
|
/// Banner title
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
/// Banner subtitle
|
/// Banner subtitle
|
||||||
final String subtitle;
|
final String subtitle;
|
||||||
|
|
||||||
/// Banner background color
|
/// Banner background color
|
||||||
final Color bg;
|
final Color bg;
|
||||||
|
|
||||||
/// Banner accent color
|
/// Banner accent color
|
||||||
final Color accent;
|
final Color accent;
|
||||||
|
|
||||||
/// Optional tap callback
|
/// Optional tap callback
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
/// Creates a [PlaceholderBanner].
|
/// 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -29,7 +38,7 @@ class PlaceholderBanner extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: bg,
|
color: bg,
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
border: Border.all(color: accent.withValues(alpha: 0.3)),
|
border: Border.all(color: accent, width: 1),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -41,7 +50,11 @@ class PlaceholderBanner extends StatelessWidget {
|
|||||||
color: UiColors.bgBanner,
|
color: UiColors.bgBanner,
|
||||||
shape: BoxShape.circle,
|
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),
|
const SizedBox(width: UiConstants.space3),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -50,12 +63,9 @@ class PlaceholderBanner extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: UiTypography.body1b,
|
style: UiTypography.body1b.copyWith(color: accent),
|
||||||
),
|
|
||||||
Text(
|
|
||||||
subtitle,
|
|
||||||
style: UiTypography.body3r.copyWith(color: UiColors.mutedForeground),
|
|
||||||
),
|
),
|
||||||
|
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_cubit.dart';
|
||||||
import '../blocs/profile_state.dart';
|
import '../blocs/profile_state.dart';
|
||||||
import '../widgets/language_selector_bottom_sheet.dart';
|
|
||||||
import '../widgets/logout_button.dart';
|
import '../widgets/logout_button.dart';
|
||||||
import '../widgets/profile_header.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_score_bar.dart';
|
||||||
import '../widgets/reliability_stats_card.dart';
|
import '../widgets/reliability_stats_card.dart';
|
||||||
import '../widgets/section_title.dart';
|
import '../widgets/sections/index.dart';
|
||||||
|
|
||||||
/// The main Staff Profile page.
|
/// The main Staff Profile page.
|
||||||
///
|
///
|
||||||
@@ -49,7 +46,6 @@ class StaffProfilePage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final i18n = Translations.of(context).staff.profile;
|
|
||||||
final ProfileCubit cubit = Modular.get<ProfileCubit>();
|
final ProfileCubit cubit = Modular.get<ProfileCubit>();
|
||||||
|
|
||||||
// Load profile data on first build
|
// Load profile data on first build
|
||||||
@@ -60,7 +56,7 @@ class StaffProfilePage extends StatelessWidget {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: BlocConsumer<ProfileCubit, ProfileState>(
|
body: BlocConsumer<ProfileCubit, ProfileState>(
|
||||||
bloc: cubit,
|
bloc: cubit,
|
||||||
listener: (context, state) {
|
listener: (BuildContext context, ProfileState state) {
|
||||||
if (state.status == ProfileStatus.signedOut) {
|
if (state.status == ProfileStatus.signedOut) {
|
||||||
Modular.to.toGetStartedPage();
|
Modular.to.toGetStartedPage();
|
||||||
} else if (state.status == ProfileStatus.error &&
|
} 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
|
// Show loading spinner if status is loading
|
||||||
if (state.status == ProfileStatus.loading) {
|
if (state.status == ProfileStatus.loading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
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) {
|
if (profile == null) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
@@ -103,7 +99,7 @@ class StaffProfilePage extends StatelessWidget {
|
|||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
padding: const EdgeInsets.only(bottom: UiConstants.space16),
|
padding: const EdgeInsets.only(bottom: UiConstants.space16),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: <Widget>[
|
||||||
ProfileHeader(
|
ProfileHeader(
|
||||||
fullName: profile.name,
|
fullName: profile.name,
|
||||||
level: _mapStatusToLevel(profile.status),
|
level: _mapStatusToLevel(profile.status),
|
||||||
@@ -117,7 +113,7 @@ class StaffProfilePage extends StatelessWidget {
|
|||||||
horizontal: UiConstants.space5,
|
horizontal: UiConstants.space5,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: <Widget>[
|
||||||
ReliabilityStatsCard(
|
ReliabilityStatsCard(
|
||||||
totalShifts: profile.totalShifts,
|
totalShifts: profile.totalShifts,
|
||||||
averageRating: profile.averageRating,
|
averageRating: profile.averageRating,
|
||||||
@@ -130,100 +126,15 @@ class StaffProfilePage extends StatelessWidget {
|
|||||||
reliabilityScore: profile.reliabilityScore,
|
reliabilityScore: profile.reliabilityScore,
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space6),
|
const SizedBox(height: UiConstants.space6),
|
||||||
SectionTitle(i18n.sections.onboarding),
|
const OnboardingSection(),
|
||||||
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 SizedBox(height: UiConstants.space6),
|
const SizedBox(height: UiConstants.space6),
|
||||||
Column(
|
const ComplianceSection(),
|
||||||
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 SizedBox(height: UiConstants.space6),
|
const SizedBox(height: UiConstants.space6),
|
||||||
SectionTitle(i18n.sections.finance),
|
const FinanceSection(),
|
||||||
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 SizedBox(height: UiConstants.space6),
|
const SizedBox(height: UiConstants.space6),
|
||||||
SectionTitle(
|
const SupportSection(),
|
||||||
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 SizedBox(height: UiConstants.space6),
|
const SizedBox(height: UiConstants.space6),
|
||||||
SectionTitle(
|
const SettingsSection(),
|
||||||
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 SizedBox(height: UiConstants.space6),
|
const SizedBox(height: UiConstants.space6),
|
||||||
LogoutButton(
|
LogoutButton(
|
||||||
onTap: () => _onSignOut(cubit, state),
|
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_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(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,9 @@ dependencies:
|
|||||||
staff_clock_in:
|
staff_clock_in:
|
||||||
path: ../clock_in
|
path: ../clock_in
|
||||||
staff_privacy_security:
|
staff_privacy_security:
|
||||||
path: ../profile_sections/settings/privacy_security
|
path: ../profile_sections/support/privacy_security
|
||||||
|
staff_faqs:
|
||||||
|
path: ../profile_sections/support/faqs
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -1290,10 +1290,17 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.12.1"
|
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:
|
staff_privacy_security:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
path: "packages/features/staff/profile_sections/settings/privacy_security"
|
path: "packages/features/staff/profile_sections/support/privacy_security"
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
Reference in New Issue
Block a user