Merge branch 'dev' of https://github.com/Oloodi/krow-workforce into feature/session-persistence-424

This commit is contained in:
2026-02-19 12:55:47 +05:30
118 changed files with 8629 additions and 234 deletions

View File

@@ -105,7 +105,7 @@ class HomeRepositoryImpl
address: staff.addres,
avatar: staff.photoUrl,
),
ownerId: session?.ownerId,
ownerId: staff.ownerId,
);
StaffSessionStore.instance.setSession(updatedSession);

View File

@@ -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],
),

View File

@@ -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),
],
),
),

View File

@@ -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,86 +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") ? "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 SupportSection(),
const SizedBox(height: UiConstants.space6),
const SettingsSection(),
const SizedBox(height: UiConstants.space6),
LogoutButton(
onTap: () => _onSignOut(cubit, state),

View File

@@ -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(),
),
],
),
],
);
}
}

View File

@@ -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(),
),
],
),
],
);
}
}

View File

@@ -0,0 +1,5 @@
export 'compliance_section.dart';
export 'finance_section.dart';
export 'onboarding_section.dart';
export 'settings_section.dart';
export 'support_section.dart';

View File

@@ -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(),
),
],
),
],
);
}
}

View File

@@ -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(),
);
},
),
],
),
],
);
}
}

View File

@@ -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(),
),
],
),
],
);
}
}

View File

@@ -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."
}
]
}
]

View File

@@ -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>[];
}
}
}

View File

@@ -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];
}

View File

@@ -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];
}

View File

@@ -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);
}

View File

@@ -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>[];
}
}
}

View File

@@ -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>[];
}
}
}

View File

@@ -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',
),
);
}
}
}

View File

@@ -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];
}

View File

@@ -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,
];
}

View File

@@ -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()]),
),
);
}
}

View File

@@ -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),
],
);
}),
],
),
);
},
);
}
}

View File

@@ -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(),
);
}
}

View File

@@ -0,0 +1,4 @@
library staff_faqs;
export 'src/staff_faqs_module.dart';
export 'src/presentation/pages/faqs_page.dart';

View File

@@ -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/

View File

@@ -0,0 +1,119 @@
PRIVACY POLICY
Effective Date: February 18, 2026
1. INTRODUCTION
Krow Workforce ("we," "us," "our," or "the App") is committed to protecting your privacy. This Privacy Policy explains how we collect, use, disclose, and otherwise process your personal information through our mobile application and related services.
2. INFORMATION WE COLLECT
2.1 Information You Provide Directly:
- Account information: name, email address, phone number, password
- Profile information: photo, bio, skills, experience, certifications
- Location data: work location preferences and current location (when enabled)
- Payment information: bank account details, tax identification numbers
- Communication data: messages, support inquiries, feedback
2.2 Information Collected Automatically:
- Device information: device type, operating system, device identifiers
- Usage data: features accessed, actions taken, time and duration of activities
- Log data: IP address, browser type, pages visited, errors encountered
- Location data: approximate location based on IP address (always)
- Precise location: only when Location Sharing is enabled
2.3 Information from Third Parties:
- Background check services: verification results
- Banking partners: account verification information
- Payment processors: transaction information
3. HOW WE USE YOUR INFORMATION
We use your information to:
- Create and maintain your account
- Process payments and verify employment eligibility
- Improve and optimize our services
- Send you important notifications and updates
- Provide customer support
- Prevent fraud and ensure security
- Comply with legal obligations
- Conduct analytics and research
- Match you with appropriate work opportunities
- Communicate promotional offers (with your consent)
4. LOCATION DATA & PRIVACY SETTINGS
4.1 Location Sharing:
You can control location sharing through Privacy Settings:
- Disabled (default): Your approximate location is based on IP address only
- Enabled: Precise location data is collected for better job matching
4.2 Your Control:
You may enable or disable precise location sharing at any time in the Privacy & Security section of your profile.
5. DATA RETENTION
We retain your personal information for as long as:
- Your account is active, plus
- An additional period as required by law or for business purposes
You may request deletion of your account and associated data by contacting support@krow.com.
6. DATA SECURITY
We implement appropriate technical and organizational measures to protect your personal information from unauthorized access, alteration, disclosure, or destruction. However, no method of transmission over the internet is 100% secure.
7. SHARING OF INFORMATION
We do not sell your personal information. We may share information with:
- Service providers and contractors: who process data on our behalf
- Employers and clients: limited information needed for job matching
- Legal authorities: when required by law
- Business partners: with your explicit consent
- Other users: your name, skills, and ratings (as needed for job matching)
8. YOUR PRIVACY RIGHTS
8.1 Access and Correction:
You have the right to access, review, and request correction of your personal information.
8.2 Data Portability:
You may request a copy of your personal data in a portable format.
8.3 Deletion:
You may request deletion of your account and personal information, subject to legal obligations.
8.4 Opt-Out:
You may opt out of marketing communications and certain data processing activities.
9. CHILDREN'S PRIVACY
Our App is not intended for individuals under 18 years of age. We do not knowingly collect personal information from children. If we become aware that we have collected information from a child, we will take steps to delete such information immediately.
10. THIRD-PARTY LINKS
Our App may contain links to third-party websites. We are not responsible for the privacy practices of these external sites. We encourage you to review their privacy policies.
11. INTERNATIONAL DATA TRANSFERS
Your information may be transferred to, stored in, and processed in countries other than your country of residence. These countries may have data protection laws different from your home country.
12. CHANGES TO THIS POLICY
We may update this Privacy Policy from time to time. We will notify you of significant changes via email or through the App. Your continued use of the App constitutes your acceptance of the updated Privacy Policy.
13. CONTACT US
If you have questions about this Privacy Policy or your personal information, please contact us at:
Email: privacy@krow.com
Address: Krow Workforce, [Company Address]
Phone: [Support Phone Number]
14. CALIFORNIA PRIVACY RIGHTS (CCPA)
If you are a California resident, you have additional rights under the California Consumer Privacy Act (CCPA). Please visit our CCPA Rights page or contact privacy@krow.com for more information.
15. EUROPEAN PRIVACY RIGHTS (GDPR)
If you are in the European Union, you have rights under the General Data Protection Regulation (GDPR). These include the right to access, rectification, erasure, and data portability. Contact privacy@krow.com to exercise these rights.

View File

@@ -0,0 +1,61 @@
TERMS OF SERVICE
Effective Date: February 18, 2026
1. ACCEPTANCE OF TERMS
By accessing and using the Krow Workforce application ("the App"), you accept and agree to be bound by the terms and provisions of this agreement. If you do not agree to abide by the above, please do not use this service.
2. USE LICENSE
Permission is granted to temporarily download one copy of the materials (information or software) on Krow Workforce's App for personal, non-commercial transitory viewing only. This is the grant of a license, not a transfer of title, and under this license you may not:
a) Modifying or copying the materials
b) Using the materials for any commercial purpose or for any public display
c) Attempting to reverse engineer, disassemble, or decompile any software contained on the App
d) Removing any copyright or other proprietary notations from the materials
e) Transferring the materials to another person or "mirroring" the materials on any other server
3. DISCLAIMER
The materials on Krow Workforce's App are provided on an "as is" basis. Krow Workforce makes no warranties, expressed or implied, and hereby disclaims and negates all other warranties including, without limitation, implied warranties or conditions of merchantability, fitness for a particular purpose, or non-infringement of intellectual property or other violation of rights.
4. LIMITATIONS
In no event shall Krow Workforce or its suppliers be liable for any damages (including, without limitation, damages for loss of data or profit, or due to business interruption) arising out of the use or inability to use the materials on Krow Workforce's App, even if Krow Workforce or a Krow Workforce authorized representative has been notified orally or in writing of the possibility of such damage.
5. ACCURACY OF MATERIALS
The materials appearing on Krow Workforce's App could include technical, typographical, or photographic errors. Krow Workforce does not warrant that any of the materials on its App are accurate, complete, or current. Krow Workforce may make changes to the materials contained on its App at any time without notice.
6. MATERIALS DISCLAIMER
Krow Workforce has not reviewed all of the sites linked to its App and is not responsible for the contents of any such linked site. The inclusion of any link does not imply endorsement by Krow Workforce of the site. Use of any such linked website is at the user's own risk.
7. MODIFICATIONS
Krow Workforce may revise these terms of service for its App at any time without notice. By using this App, you are agreeing to be bound by the then current version of these terms of service.
8. GOVERNING LAW
These terms and conditions are governed by and construed in accordance with the laws of the jurisdiction in which Krow Workforce is located, and you irrevocably submit to the exclusive jurisdiction of the courts in that location.
9. LIMITATION OF LIABILITY
In no case shall Krow Workforce, its staff, or other contributors be liable for any indirect, incidental, consequential, special, or punitive damages arising out of or relating to the use of the App.
10. USER CONTENT
You grant Krow Workforce a non-exclusive, royalty-free, perpetual, and irrevocable right to use any content you provide to us, including but not limited to text, images, and information, in any media or format and for any purpose consistent with our business.
11. INDEMNIFICATION
You agree to indemnify and hold harmless Krow Workforce and its staff from any and all claims, damages, losses, costs, and expenses, including attorney's fees, arising out of or resulting from your use of the App or violation of these terms.
12. TERMINATION
Krow Workforce reserves the right to terminate your account and access to the App at any time, in its sole discretion, for any reason or no reason, with or without notice.
13. CONTACT INFORMATION
If you have any questions about these Terms of Service, please contact us at support@krow.com.

View File

@@ -0,0 +1,92 @@
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
import 'package:flutter/services.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import '../../domain/repositories/privacy_settings_repository_interface.dart';
/// Data layer implementation of privacy settings repository
///
/// Handles all backend communication for privacy settings via Data Connect,
/// and loads legal documents from app assets
class PrivacySettingsRepositoryImpl
implements PrivacySettingsRepositoryInterface {
PrivacySettingsRepositoryImpl(this._service);
final DataConnectService _service;
@override
Future<bool> getProfileVisibility() async {
return _service.run<bool>(() async {
// Get current user ID
final String staffId = await _service.getStaffId();
// Call Data Connect query: getStaffProfileVisibility
final fdc.QueryResult<
GetStaffProfileVisibilityData,
GetStaffProfileVisibilityVariables
>
response = await _service.connector
.getStaffProfileVisibility(staffId: staffId)
.execute();
// Return the profile visibility status from the first result
if (response.data.staff != null) {
return response.data.staff?.isProfileVisible ?? true;
}
// Default to visible if no staff record found
return true;
});
}
@override
Future<bool> updateProfileVisibility(bool isVisible) async {
return _service.run<bool>(() async {
// Get staff ID for the current user
final String staffId = await _service.getStaffId();
// Call Data Connect mutation: UpdateStaffProfileVisibility
await _service.connector
.updateStaffProfileVisibility(
id: staffId,
isProfileVisible: isVisible,
)
.execute();
// Return the requested visibility state
return isVisible;
});
}
@override
Future<String> getTermsOfService() async {
return _service.run<String>(() async {
try {
// Load from package asset path
return await rootBundle.loadString(
'packages/staff_privacy_security/lib/src/assets/legal/terms_of_service.txt',
);
} catch (e) {
// Final fallback if asset not found
print('Error loading terms of service: $e');
return 'Terms of Service - Content unavailable. Please contact support@krow.com';
}
});
}
@override
Future<String> getPrivacyPolicy() async {
return _service.run<String>(() async {
try {
// Load from package asset path
return await rootBundle.loadString(
'packages/staff_privacy_security/lib/src/assets/legal/privacy_policy.txt',
);
} catch (e) {
// Final fallback if asset not found
print('Error loading privacy policy: $e');
return 'Privacy Policy - Content unavailable. Please contact privacy@krow.com';
}
});
}
}

View File

@@ -0,0 +1,29 @@
import 'package:equatable/equatable.dart';
/// Privacy settings entity representing user privacy preferences
class PrivacySettingsEntity extends Equatable {
/// Whether location sharing during shifts is enabled
final bool locationSharing;
/// The timestamp when these settings were last updated
final DateTime? updatedAt;
const PrivacySettingsEntity({
required this.locationSharing,
this.updatedAt,
});
/// Create a copy with optional field overrides
PrivacySettingsEntity copyWith({
bool? locationSharing,
DateTime? updatedAt,
}) {
return PrivacySettingsEntity(
locationSharing: locationSharing ?? this.locationSharing,
updatedAt: updatedAt ?? this.updatedAt,
);
}
@override
List<Object?> get props => [locationSharing, updatedAt];
}

View File

@@ -0,0 +1,16 @@
/// Interface for privacy settings repository operations
abstract class PrivacySettingsRepositoryInterface {
/// Fetch the current staff member's profile visibility setting
Future<bool> getProfileVisibility();
/// Update profile visibility preference
///
/// Returns the updated profile visibility status
Future<bool> updateProfileVisibility(bool isVisible);
/// Fetch terms of service content
Future<String> getTermsOfService();
/// Fetch privacy policy content
Future<String> getPrivacyPolicy();
}

View File

@@ -0,0 +1,17 @@
import '../repositories/privacy_settings_repository_interface.dart';
/// Use case to retrieve privacy policy
class GetPrivacyPolicyUseCase {
final PrivacySettingsRepositoryInterface _repository;
GetPrivacyPolicyUseCase(this._repository);
/// Execute the use case to get privacy policy
Future<String> call() async {
try {
return await _repository.getPrivacyPolicy();
} catch (e) {
return 'Privacy Policy is currently unavailable.';
}
}
}

View File

@@ -0,0 +1,19 @@
import '../repositories/privacy_settings_repository_interface.dart';
/// Use case to retrieve the current staff member's profile visibility setting
class GetProfileVisibilityUseCase {
final PrivacySettingsRepositoryInterface _repository;
GetProfileVisibilityUseCase(this._repository);
/// Execute the use case to get profile visibility status
/// Returns true if profile is visible, false if hidden
Future<bool> call() async {
try {
return await _repository.getProfileVisibility();
} catch (e) {
// Return default (visible) on error
return true;
}
}
}

View File

@@ -0,0 +1,17 @@
import '../repositories/privacy_settings_repository_interface.dart';
/// Use case to retrieve terms of service
class GetTermsUseCase {
final PrivacySettingsRepositoryInterface _repository;
GetTermsUseCase(this._repository);
/// Execute the use case to get terms of service
Future<String> call() async {
try {
return await _repository.getTermsOfService();
} catch (e) {
return 'Terms of Service is currently unavailable.';
}
}
}

View File

@@ -0,0 +1,32 @@
import 'package:equatable/equatable.dart';
import '../repositories/privacy_settings_repository_interface.dart';
/// Parameters for updating profile visibility
class UpdateProfileVisibilityParams extends Equatable {
/// Whether to show (true) or hide (false) the profile
final bool isVisible;
const UpdateProfileVisibilityParams({required this.isVisible});
@override
List<Object?> get props => <Object?>[isVisible];
}
/// Use case to update profile visibility setting
class UpdateProfileVisibilityUseCase {
final PrivacySettingsRepositoryInterface _repository;
UpdateProfileVisibilityUseCase(this._repository);
/// Execute the use case to update profile visibility
/// Returns the updated visibility status
Future<bool> call(UpdateProfileVisibilityParams params) async {
try {
return await _repository.updateProfileVisibility(params.isVisible);
} catch (e) {
// Return the requested state on error (optimistic)
return params.isVisible;
}
}
}

View File

@@ -0,0 +1,55 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../domain/usecases/get_privacy_policy_usecase.dart';
/// State for Privacy Policy cubit
class PrivacyPolicyState {
final String? content;
final bool isLoading;
final String? error;
const PrivacyPolicyState({
this.content,
this.isLoading = false,
this.error,
});
PrivacyPolicyState copyWith({
String? content,
bool? isLoading,
String? error,
}) {
return PrivacyPolicyState(
content: content ?? this.content,
isLoading: isLoading ?? this.isLoading,
error: error ?? this.error,
);
}
}
/// Cubit for managing Privacy Policy content
class PrivacyPolicyCubit extends Cubit<PrivacyPolicyState> {
final GetPrivacyPolicyUseCase _getPrivacyPolicyUseCase;
PrivacyPolicyCubit({
required GetPrivacyPolicyUseCase getPrivacyPolicyUseCase,
}) : _getPrivacyPolicyUseCase = getPrivacyPolicyUseCase,
super(const PrivacyPolicyState());
/// Fetch privacy policy content
Future<void> fetchPrivacyPolicy() async {
emit(state.copyWith(isLoading: true, error: null));
try {
final String content = await _getPrivacyPolicyUseCase();
emit(state.copyWith(
content: content,
isLoading: false,
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
error: e.toString(),
));
}
}
}

View File

@@ -0,0 +1,55 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../domain/usecases/get_terms_usecase.dart';
/// State for Terms of Service cubit
class TermsState {
final String? content;
final bool isLoading;
final String? error;
const TermsState({
this.content,
this.isLoading = false,
this.error,
});
TermsState copyWith({
String? content,
bool? isLoading,
String? error,
}) {
return TermsState(
content: content ?? this.content,
isLoading: isLoading ?? this.isLoading,
error: error ?? this.error,
);
}
}
/// Cubit for managing Terms of Service content
class TermsCubit extends Cubit<TermsState> {
final GetTermsUseCase _getTermsUseCase;
TermsCubit({
required GetTermsUseCase getTermsUseCase,
}) : _getTermsUseCase = getTermsUseCase,
super(const TermsState());
/// Fetch terms of service content
Future<void> fetchTerms() async {
emit(state.copyWith(isLoading: true, error: null));
try {
final String content = await _getTermsUseCase();
emit(state.copyWith(
content: content,
isLoading: false,
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
error: e.toString(),
));
}
}
}

View File

@@ -0,0 +1,143 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import '../../domain/usecases/get_profile_visibility_usecase.dart';
import '../../domain/usecases/update_profile_visibility_usecase.dart';
import '../../domain/usecases/get_terms_usecase.dart';
import '../../domain/usecases/get_privacy_policy_usecase.dart';
part 'privacy_security_event.dart';
part 'privacy_security_state.dart';
/// BLoC managing privacy and security settings state
class PrivacySecurityBloc
extends Bloc<PrivacySecurityEvent, PrivacySecurityState> {
final GetProfileVisibilityUseCase _getProfileVisibilityUseCase;
final UpdateProfileVisibilityUseCase _updateProfileVisibilityUseCase;
final GetTermsUseCase _getTermsUseCase;
final GetPrivacyPolicyUseCase _getPrivacyPolicyUseCase;
PrivacySecurityBloc({
required GetProfileVisibilityUseCase getProfileVisibilityUseCase,
required UpdateProfileVisibilityUseCase updateProfileVisibilityUseCase,
required GetTermsUseCase getTermsUseCase,
required GetPrivacyPolicyUseCase getPrivacyPolicyUseCase,
}) : _getProfileVisibilityUseCase = getProfileVisibilityUseCase,
_updateProfileVisibilityUseCase = updateProfileVisibilityUseCase,
_getTermsUseCase = getTermsUseCase,
_getPrivacyPolicyUseCase = getPrivacyPolicyUseCase,
super(const PrivacySecurityState()) {
on<FetchProfileVisibilityEvent>(_onFetchProfileVisibility);
on<UpdateProfileVisibilityEvent>(_onUpdateProfileVisibility);
on<FetchTermsEvent>(_onFetchTerms);
on<FetchPrivacyPolicyEvent>(_onFetchPrivacyPolicy);
on<ClearProfileVisibilityUpdatedEvent>(_onClearProfileVisibilityUpdated);
}
Future<void> _onFetchProfileVisibility(
FetchProfileVisibilityEvent event,
Emitter<PrivacySecurityState> emit,
) async {
emit(state.copyWith(isLoading: true, error: null));
try {
final bool isVisible = await _getProfileVisibilityUseCase.call();
emit(
state.copyWith(
isLoading: false,
isProfileVisible: isVisible,
),
);
} catch (e) {
emit(
state.copyWith(
isLoading: false,
error: 'Failed to fetch profile visibility',
),
);
}
}
Future<void> _onUpdateProfileVisibility(
UpdateProfileVisibilityEvent event,
Emitter<PrivacySecurityState> emit,
) async {
emit(state.copyWith(isUpdating: true, error: null, profileVisibilityUpdated: false));
try {
final bool isVisible = await _updateProfileVisibilityUseCase.call(
UpdateProfileVisibilityParams(isVisible: event.isVisible),
);
emit(
state.copyWith(
isUpdating: false,
isProfileVisible: isVisible,
profileVisibilityUpdated: true,
),
);
} catch (e) {
emit(
state.copyWith(
isUpdating: false,
error: 'Failed to update profile visibility',
profileVisibilityUpdated: false,
),
);
}
}
Future<void> _onFetchTerms(
FetchTermsEvent event,
Emitter<PrivacySecurityState> emit,
) async {
emit(state.copyWith(isLoadingTerms: true, error: null));
try {
final String content = await _getTermsUseCase.call();
emit(
state.copyWith(
isLoadingTerms: false,
termsContent: content,
),
);
} catch (e) {
emit(
state.copyWith(
isLoadingTerms: false,
error: 'Failed to fetch terms of service',
),
);
}
}
Future<void> _onFetchPrivacyPolicy(
FetchPrivacyPolicyEvent event,
Emitter<PrivacySecurityState> emit,
) async {
emit(state.copyWith(isLoadingPrivacyPolicy: true, error: null));
try {
final String content = await _getPrivacyPolicyUseCase.call();
emit(
state.copyWith(
isLoadingPrivacyPolicy: false,
privacyPolicyContent: content,
),
);
} catch (e) {
emit(
state.copyWith(
isLoadingPrivacyPolicy: false,
error: 'Failed to fetch privacy policy',
),
);
}
}
void _onClearProfileVisibilityUpdated(
ClearProfileVisibilityUpdatedEvent event,
Emitter<PrivacySecurityState> emit,
) {
emit(state.copyWith(profileVisibilityUpdated: false));
}
}

View File

@@ -0,0 +1,40 @@
part of 'privacy_security_bloc.dart';
/// Base class for privacy security BLoC events
abstract class PrivacySecurityEvent extends Equatable {
const PrivacySecurityEvent();
@override
List<Object?> get props => <Object?>[];
}
/// Event to fetch current profile visibility setting
class FetchProfileVisibilityEvent extends PrivacySecurityEvent {
const FetchProfileVisibilityEvent();
}
/// Event to update profile visibility
class UpdateProfileVisibilityEvent extends PrivacySecurityEvent {
/// Whether to show (true) or hide (false) the profile
final bool isVisible;
const UpdateProfileVisibilityEvent({required this.isVisible});
@override
List<Object?> get props => <Object?>[isVisible];
}
/// Event to fetch terms of service
class FetchTermsEvent extends PrivacySecurityEvent {
const FetchTermsEvent();
}
/// Event to fetch privacy policy
class FetchPrivacyPolicyEvent extends PrivacySecurityEvent {
const FetchPrivacyPolicyEvent();
}
/// Event to clear the profile visibility updated flag after showing snackbar
class ClearProfileVisibilityUpdatedEvent extends PrivacySecurityEvent {
const ClearProfileVisibilityUpdatedEvent();
}

View File

@@ -0,0 +1,83 @@
part of 'privacy_security_bloc.dart';
/// State for privacy security BLoC
class PrivacySecurityState extends Equatable {
/// Current profile visibility setting (true = visible, false = hidden)
final bool isProfileVisible;
/// Whether profile visibility is currently loading
final bool isLoading;
/// Whether profile visibility is currently being updated
final bool isUpdating;
/// Whether the profile visibility was just successfully updated
final bool profileVisibilityUpdated;
/// Terms of service content
final String? termsContent;
/// Whether terms are currently loading
final bool isLoadingTerms;
/// Privacy policy content
final String? privacyPolicyContent;
/// Whether privacy policy is currently loading
final bool isLoadingPrivacyPolicy;
/// Error message, if any
final String? error;
const PrivacySecurityState({
this.isProfileVisible = true,
this.isLoading = false,
this.isUpdating = false,
this.profileVisibilityUpdated = false,
this.termsContent,
this.isLoadingTerms = false,
this.privacyPolicyContent,
this.isLoadingPrivacyPolicy = false,
this.error,
});
/// Create a copy with optional field overrides
PrivacySecurityState copyWith({
bool? isProfileVisible,
bool? isLoading,
bool? isUpdating,
bool? profileVisibilityUpdated,
String? termsContent,
bool? isLoadingTerms,
String? privacyPolicyContent,
bool? isLoadingPrivacyPolicy,
String? error,
}) {
return PrivacySecurityState(
isProfileVisible: isProfileVisible ?? this.isProfileVisible,
isLoading: isLoading ?? this.isLoading,
isUpdating: isUpdating ?? this.isUpdating,
profileVisibilityUpdated: profileVisibilityUpdated ?? this.profileVisibilityUpdated,
termsContent: termsContent ?? this.termsContent,
isLoadingTerms: isLoadingTerms ?? this.isLoadingTerms,
privacyPolicyContent: privacyPolicyContent ?? this.privacyPolicyContent,
isLoadingPrivacyPolicy:
isLoadingPrivacyPolicy ?? this.isLoadingPrivacyPolicy,
error: error,
);
}
@override
List<Object?> get props => <Object?>[
isProfileVisible,
isLoading,
isUpdating,
profileVisibilityUpdated,
termsContent,
isLoadingTerms,
privacyPolicyContent,
isLoadingPrivacyPolicy,
error,
];
}

View File

@@ -0,0 +1,66 @@
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/legal/privacy_policy_cubit.dart';
/// Page displaying the Privacy Policy document
class PrivacyPolicyPage extends StatelessWidget {
const PrivacyPolicyPage({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: UiAppBar(
title: t.staff_privacy_security.privacy_policy.title,
showBackButton: true,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1),
child: Container(color: UiColors.border, height: 1),
),
),
body: BlocProvider<PrivacyPolicyCubit>(
create: (BuildContext context) => Modular.get<PrivacyPolicyCubit>()..fetchPrivacyPolicy(),
child: BlocBuilder<PrivacyPolicyCubit, PrivacyPolicyState>(
builder: (BuildContext context, PrivacyPolicyState state) {
if (state.isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (state.error != null) {
return Center(
child: Padding(
padding: const EdgeInsets.all(UiConstants.space5),
child: Text(
'Error loading Privacy Policy: ${state.error}',
textAlign: TextAlign.center,
style: UiTypography.body2r.copyWith(
color: UiColors.textSecondary,
),
),
),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.all(UiConstants.space5),
child: Text(
state.content ?? 'No content available',
style: UiTypography.body2r.copyWith(
height: 1.6,
color: UiColors.textPrimary,
),
),
);
},
),
),
);
}
}

View File

@@ -0,0 +1,66 @@
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/legal/terms_cubit.dart';
/// Page displaying the Terms of Service document
class TermsOfServicePage extends StatelessWidget {
const TermsOfServicePage({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: UiAppBar(
title: t.staff_privacy_security.terms_of_service.title,
showBackButton: true,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1),
child: Container(color: UiColors.border, height: 1),
),
),
body: BlocProvider<TermsCubit>(
create: (BuildContext context) => Modular.get<TermsCubit>()..fetchTerms(),
child: BlocBuilder<TermsCubit, TermsState>(
builder: (BuildContext context, TermsState state) {
if (state.isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (state.error != null) {
return Center(
child: Padding(
padding: const EdgeInsets.all(UiConstants.space5),
child: Text(
'Error loading Terms of Service: ${state.error}',
textAlign: TextAlign.center,
style: UiTypography.body2r.copyWith(
color: UiColors.textSecondary,
),
),
),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.all(UiConstants.space5),
child: Text(
state.content ?? 'No content available',
style: UiTypography.body2r.copyWith(
height: 1.6,
color: UiColors.textPrimary,
),
),
);
},
),
),
);
}
}

View File

@@ -0,0 +1,53 @@
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/privacy_security_bloc.dart';
import '../widgets/legal/legal_section_widget.dart';
import '../widgets/privacy/privacy_section_widget.dart';
/// Page displaying privacy & security settings for staff
class PrivacySecurityPage extends StatelessWidget {
const PrivacySecurityPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: UiAppBar(
title: t.staff_privacy_security.title,
showBackButton: true,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1),
child: Container(color: UiColors.border, height: 1),
),
),
body: BlocProvider<PrivacySecurityBloc>.value(
value: Modular.get<PrivacySecurityBloc>()
..add(const FetchProfileVisibilityEvent()),
child: BlocBuilder<PrivacySecurityBloc, PrivacySecurityState>(
builder: (BuildContext context, PrivacySecurityState state) {
if (state.isLoading) {
return const UiLoadingPage();
}
return const SingleChildScrollView(
padding: EdgeInsets.all(UiConstants.space6),
child: Column(
spacing: UiConstants.space6,
children: <Widget>[
// Privacy Section
PrivacySectionWidget(),
// Legal Section
LegalSectionWidget(),
],
),
);
},
),
),
);
}
}

View File

@@ -0,0 +1,70 @@
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 'package:krow_core/core.dart';
import '../../blocs/privacy_security_bloc.dart';
import '../settings_action_tile_widget.dart';
import '../settings_divider_widget.dart';
import '../settings_section_header_widget.dart';
/// Widget displaying legal documents (Terms of Service and Privacy Policy)
class LegalSectionWidget extends StatelessWidget {
const LegalSectionWidget({super.key});
@override
Widget build(BuildContext context) {
return Column(
spacing: UiConstants.space4,
children: <Widget>[
// Legal Section Header
SettingsSectionHeader(
title: t.staff_privacy_security.legal_section,
icon: UiIcons.shield,
),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
border: Border.all(color: UiColors.border),
),
child: Column(
children: <Widget>[
SettingsActionTile(
title: t.staff_privacy_security.terms_of_service.title,
onTap: () => _navigateToTerms(context),
),
const SettingsDivider(),
SettingsActionTile(
title: t.staff_privacy_security.privacy_policy.title,
onTap: () => _navigateToPrivacyPolicy(context),
),
],
),
),
],
);
}
/// Navigate to terms of service page
void _navigateToTerms(BuildContext context) {
BlocProvider.of<PrivacySecurityBloc>(context).add(const FetchTermsEvent());
// Navigate using typed navigator
Modular.to.toTermsOfService();
}
/// Navigate to privacy policy page
void _navigateToPrivacyPolicy(BuildContext context) {
BlocProvider.of<PrivacySecurityBloc>(
context,
).add(const FetchPrivacyPolicyEvent());
// Navigate using typed navigator
Modular.to.toPrivacyPolicy();
}
}

View File

@@ -0,0 +1,70 @@
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 '../../blocs/privacy_security_bloc.dart';
import '../settings_section_header_widget.dart';
import '../settings_switch_tile_widget.dart';
/// Widget displaying privacy settings including profile visibility preference
class PrivacySectionWidget extends StatelessWidget {
const PrivacySectionWidget({super.key});
@override
Widget build(BuildContext context) {
return BlocListener<PrivacySecurityBloc, PrivacySecurityState>(
listener: (BuildContext context, PrivacySecurityState state) {
// Show success message when profile visibility update just completed
if (state.profileVisibilityUpdated && state.error == null) {
UiSnackbar.show(
context,
message: t.staff_privacy_security.success.profile_visibility_updated,
type: UiSnackbarType.success,
);
// Clear the flag after showing the snackbar
context.read<PrivacySecurityBloc>().add(
const ClearProfileVisibilityUpdatedEvent(),
);
}
},
child: BlocBuilder<PrivacySecurityBloc, PrivacySecurityState>(
builder: (BuildContext context, PrivacySecurityState state) {
return Column(
children: <Widget>[
// Privacy Section Header
SettingsSectionHeader(
title: t.staff_privacy_security.privacy_section,
icon: UiIcons.eye,
),
const SizedBox(height: 12.0),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
border: Border.all(
color: UiColors.border,
),
),
child: Column(
children: <Widget>[
SettingsSwitchTile(
title: t.staff_privacy_security.profile_visibility.title,
subtitle: t.staff_privacy_security.profile_visibility.subtitle,
value: state.isProfileVisible,
onChanged: (bool value) {
BlocProvider.of<PrivacySecurityBloc>(context).add(
UpdateProfileVisibilityEvent(isVisible: value),
);
},
),
],
),
),
],
);
},
),
);
}
}

View File

@@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:design_system/design_system.dart';
/// Reusable widget for action tile (tap to navigate)
class SettingsActionTile extends StatelessWidget {
/// The title of the action
final String title;
/// Optional subtitle describing the action
final String? subtitle;
/// Callback when tile is tapped
final VoidCallback onTap;
const SettingsActionTile({
Key? key,
required this.title,
this.subtitle,
required this.onTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(UiConstants.space4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
title,
style: UiTypography.body2r.copyWith(
fontWeight: FontWeight.w500,
),
),
if (subtitle != null) ...<Widget>[
SizedBox(height: UiConstants.space1),
Text(
subtitle!,
style: UiTypography.footnote1r.copyWith(
color: UiColors.muted,
),
),
],
],
),
),
const Icon(
UiIcons.chevronRight,
size: 16,
color: UiColors.iconSecondary,
),
],
),
),
);
}
}

View File

@@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
import 'package:design_system/design_system.dart';
/// Divider widget for separating items within settings sections
class SettingsDivider extends StatelessWidget {
const SettingsDivider({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Divider(
height: 1,
color: UiColors.border,
);
}
}

View File

@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:design_system/design_system.dart';
/// Reusable widget for settings section header with icon
class SettingsSectionHeader extends StatelessWidget {
/// The title of the section
final String title;
/// The icon to display next to the title
final IconData icon;
const SettingsSectionHeader({
Key? key,
required this.title,
required this.icon,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Icon(
icon,
size: 20,
color: UiColors.primary,
),
SizedBox(width: UiConstants.space2),
Text(
title,
style: UiTypography.body1r.copyWith(
fontWeight: FontWeight.w600,
),
),
],
);
}
}

View File

@@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:design_system/design_system.dart';
/// Reusable widget for toggle tile in privacy settings
class SettingsSwitchTile extends StatelessWidget {
/// The title of the setting
final String title;
/// The subtitle describing the setting
final String subtitle;
/// Current toggle value
final bool value;
/// Callback when toggle is changed
final ValueChanged<bool> onChanged;
const SettingsSwitchTile({
Key? key,
required this.title,
required this.subtitle,
required this.value,
required this.onChanged,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(UiConstants.space4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(title, style: UiTypography.body2r),
Text(subtitle, style: UiTypography.footnote1r.textSecondary),
],
),
),
Switch(value: value, onChanged: onChanged),
],
),
);
}
}

View File

@@ -0,0 +1,109 @@
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'data/repositories_impl/privacy_settings_repository_impl.dart';
import 'domain/repositories/privacy_settings_repository_interface.dart';
import 'domain/usecases/get_privacy_policy_usecase.dart';
import 'domain/usecases/get_profile_visibility_usecase.dart';
import 'domain/usecases/get_terms_usecase.dart';
import 'domain/usecases/update_profile_visibility_usecase.dart';
import 'presentation/blocs/legal/privacy_policy_cubit.dart';
import 'presentation/blocs/legal/terms_cubit.dart';
import 'presentation/blocs/privacy_security_bloc.dart';
import 'presentation/pages/legal/privacy_policy_page.dart';
import 'presentation/pages/legal/terms_of_service_page.dart';
import 'presentation/pages/privacy_security_page.dart';
/// Module for privacy security feature
///
/// Provides:
/// - Dependency injection for repositories, use cases, and BLoCs
/// - Route definitions delegated to core routing
class PrivacySecurityModule extends Module {
@override
void binds(Injector i) {
// Repository
i.addSingleton<PrivacySettingsRepositoryInterface>(
() => PrivacySettingsRepositoryImpl(
Modular.get<DataConnectService>(),
),
);
// Use Cases
i.addSingleton(
() => GetProfileVisibilityUseCase(
i<PrivacySettingsRepositoryInterface>(),
),
);
i.addSingleton(
() => UpdateProfileVisibilityUseCase(
i<PrivacySettingsRepositoryInterface>(),
),
);
i.addSingleton(
() => GetTermsUseCase(
i<PrivacySettingsRepositoryInterface>(),
),
);
i.addSingleton(
() => GetPrivacyPolicyUseCase(
i<PrivacySettingsRepositoryInterface>(),
),
);
// BLoC
i.add(
() => PrivacySecurityBloc(
getProfileVisibilityUseCase: i(),
updateProfileVisibilityUseCase: i(),
getTermsUseCase: i(),
getPrivacyPolicyUseCase: i(),
),
);
// Legal Cubits
i.add(
() => TermsCubit(
getTermsUseCase: i<GetTermsUseCase>(),
),
);
i.add(
() => PrivacyPolicyCubit(
getPrivacyPolicyUseCase: i<GetPrivacyPolicyUseCase>(),
),
);
}
@override
void routes(RouteManager r) {
// Main privacy security page
r.child(
StaffPaths.childRoute(
StaffPaths.privacySecurity,
StaffPaths.privacySecurity,
),
child: (BuildContext context) => const PrivacySecurityPage(),
);
// Terms of Service page
r.child(
StaffPaths.childRoute(
StaffPaths.privacySecurity,
StaffPaths.termsOfService,
),
child: (BuildContext context) => const TermsOfServicePage(),
);
// Privacy Policy page
r.child(
StaffPaths.childRoute(
StaffPaths.privacySecurity,
StaffPaths.privacyPolicy,
),
child: (BuildContext context) => const PrivacyPolicyPage(),
);
}
}

View File

@@ -0,0 +1,12 @@
export 'src/domain/entities/privacy_settings_entity.dart';
export 'src/domain/repositories/privacy_settings_repository_interface.dart';
export 'src/domain/usecases/get_terms_usecase.dart';
export 'src/domain/usecases/get_privacy_policy_usecase.dart';
export 'src/data/repositories_impl/privacy_settings_repository_impl.dart';
export 'src/presentation/blocs/privacy_security_bloc.dart';
export 'src/presentation/pages/privacy_security_page.dart';
export 'src/presentation/widgets/settings_switch_tile_widget.dart';
export 'src/presentation/widgets/settings_action_tile_widget.dart';
export 'src/presentation/widgets/settings_section_header_widget.dart';
export 'src/presentation/widgets/settings_divider_widget.dart';
export 'src/staff_privacy_security_module.dart';

View File

@@ -0,0 +1,42 @@
name: staff_privacy_security
description: Privacy & Security settings 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
firebase_data_connect: ^0.2.2+1
url_launcher: ^6.2.0
# Architecture Packages
krow_domain:
path: ../../../../../domain
krow_data_connect:
path: ../../../../../data_connect
krow_core:
path: ../../../../../core
design_system:
path: ../../../../../design_system
core_localization:
path: ../../../../../core_localization
dev_dependencies:
flutter_test:
sdk: flutter
bloc_test: ^9.1.0
mocktail: ^1.0.0
flutter:
uses-material-design: true
assets:
- lib/src/assets/legal/

View File

@@ -15,6 +15,8 @@ class ShiftsRepositoryImpl
// Cache: ApplicationID -> RoleID (For Accept/Decline w/ Update mutation)
final Map<String, String> _appToRoleIdMap = {};
// This need to be an APPLICATION
// THERE SHOULD BE APPLICATIONSTATUS and SHIFTSTATUS enums in the domain layer to avoid this string mapping and potential bugs.
@override
Future<List<Shift>> getMyShifts({
required DateTime start,
@@ -187,8 +189,6 @@ class ShiftsRepositoryImpl
.listShiftRolesByVendorId(vendorId: vendorId)
.execute());
final allShiftRoles = result.data.shiftRoles;
// Fetch my applications to filter out already booked shifts

View File

@@ -175,30 +175,30 @@ class _ShiftsPageState extends State<ShiftsPage> {
child: state is ShiftsLoading
? const Center(child: CircularProgressIndicator())
: state is ShiftsError
? Center(
child: Padding(
padding: const EdgeInsets.all(UiConstants.space5),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
translateErrorKey(state.message),
style: UiTypography.body2r.textSecondary,
textAlign: TextAlign.center,
),
],
? Center(
child: Padding(
padding: const EdgeInsets.all(UiConstants.space5),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
translateErrorKey(state.message),
style: UiTypography.body2r.textSecondary,
textAlign: TextAlign.center,
),
),
)
: _buildTabContent(
],
),
),
)
: _buildTabContent(
myShifts,
pendingAssignments,
cancelledShifts,
availableJobs,
historyShifts,
availableLoading,
historyLoading,
),
pendingAssignments,
cancelledShifts,
availableJobs,
historyShifts,
availableLoading,
historyLoading,
),
),
],
),
@@ -254,14 +254,14 @@ class _ShiftsPageState extends State<ShiftsPage> {
onTap: !enabled
? null
: () {
setState(() => _activeTab = id);
if (id == 'history') {
_bloc.add(LoadHistoryShiftsEvent());
}
if (id == 'find') {
_bloc.add(LoadAvailableShiftsEvent());
}
},
setState(() => _activeTab = id);
if (id == 'history') {
_bloc.add(LoadHistoryShiftsEvent());
}
if (id == 'find') {
_bloc.add(LoadAvailableShiftsEvent());
}
},
child: Container(
padding: const EdgeInsets.symmetric(
vertical: UiConstants.space2,
@@ -290,9 +290,17 @@ class _ShiftsPageState extends State<ShiftsPage> {
Flexible(
child: Text(
label,
style: (isActive ? UiTypography.body3m.copyWith(color: UiColors.primary) : UiTypography.body3m.white).copyWith(
color: !enabled ? UiColors.white.withValues(alpha: 0.5) : null,
),
style:
(isActive
? UiTypography.body3m.copyWith(
color: UiColors.primary,
)
: UiTypography.body3m.white)
.copyWith(
color: !enabled
? UiColors.white.withValues(alpha: 0.5)
: null,
),
overflow: TextOverflow.ellipsis,
),
),

View File

@@ -32,6 +32,8 @@ dependencies:
url_launcher: ^6.3.1
firebase_auth: ^6.1.4
firebase_data_connect: ^0.2.2+2
meta: ^1.17.0
bloc: ^8.1.4
dev_dependencies:
flutter_test:

View File

@@ -12,11 +12,13 @@ import 'package:staff_home/staff_home.dart';
import 'package:staff_main/src/presentation/blocs/staff_main_cubit.dart';
import 'package:staff_main/src/presentation/pages/staff_main_page.dart';
import 'package:staff_payments/staff_payements.dart';
import 'package:staff_privacy_security/staff_privacy_security.dart';
import 'package:staff_profile/staff_profile.dart';
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 {
@@ -93,9 +95,17 @@ class StaffMainModule extends Module {
StaffPaths.childRoute(StaffPaths.main, StaffPaths.availability),
module: StaffAvailabilityModule(),
);
r.module(
StaffPaths.childRoute(StaffPaths.main, StaffPaths.privacySecurity),
module: PrivacySecurityModule(),
);
r.module(
StaffPaths.childRoute(StaffPaths.main, StaffPaths.shiftDetailsRoute),
module: ShiftDetailsModule(),
);
r.module(
StaffPaths.childRoute(StaffPaths.main, StaffPaths.faqs),
module: FaqsModule(),
);
}
}

View File

@@ -20,6 +20,8 @@ dependencies:
path: ../../../design_system
core_localization:
path: ../../../core_localization
krow_core:
path: ../../../krow_core
# Features
staff_home:
@@ -52,6 +54,10 @@ dependencies:
path: ../availability
staff_clock_in:
path: ../clock_in
staff_privacy_security:
path: ../profile_sections/support/privacy_security
staff_faqs:
path: ../profile_sections/support/faqs
dev_dependencies:
flutter_test: