Merge branch 'dev' into feature/session-persistence-new
This commit is contained in:
@@ -155,7 +155,7 @@ class _PhoneVerificationPageState extends State<PhoneVerificationPage> {
|
||||
BlocProvider.of<AuthBloc>(
|
||||
context,
|
||||
).add(AuthResetRequested(mode: widget.mode));
|
||||
Navigator.of(context).pop();
|
||||
Modular.to.popSafe();;
|
||||
},
|
||||
),
|
||||
body: SafeArea(
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
import '../blocs/availability_bloc.dart';
|
||||
import '../blocs/availability_event.dart';
|
||||
import '../blocs/availability_state.dart';
|
||||
import '../widgets/availability_page_skeleton/availability_page_skeleton.dart';
|
||||
|
||||
class AvailabilityPage extends StatefulWidget {
|
||||
const AvailabilityPage({super.key});
|
||||
@@ -72,7 +73,7 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
child: BlocBuilder<AvailabilityBloc, AvailabilityState>(
|
||||
builder: (context, state) {
|
||||
if (state is AvailabilityLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
return const AvailabilityPageSkeleton();
|
||||
} else if (state is AvailabilityLoaded) {
|
||||
return Stack(
|
||||
children: [
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export 'availability_page_skeleton/index.dart';
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'day_availability_skeleton.dart';
|
||||
import 'info_card_skeleton.dart';
|
||||
import 'quick_set_skeleton.dart';
|
||||
import 'week_navigation_skeleton.dart';
|
||||
|
||||
/// Shimmer loading skeleton for the availability page.
|
||||
///
|
||||
/// Mimics the loaded layout: quick-set buttons, week navigation calendar,
|
||||
/// selected day detail with time-slot rows, and an info card.
|
||||
class AvailabilityPageSkeleton extends StatelessWidget {
|
||||
/// Creates an [AvailabilityPageSkeleton].
|
||||
const AvailabilityPageSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiShimmer(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
),
|
||||
child: Column(
|
||||
spacing: UiConstants.space6,
|
||||
children: const [
|
||||
QuickSetSkeleton(),
|
||||
WeekNavigationSkeleton(),
|
||||
DayAvailabilitySkeleton(),
|
||||
InfoCardSkeleton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for the selected day detail card (header + time slot rows).
|
||||
class DayAvailabilitySkeleton extends StatelessWidget {
|
||||
/// Creates a [DayAvailabilitySkeleton].
|
||||
const DayAvailabilitySkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Header: date text + toggle placeholder
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const [
|
||||
UiShimmerLine(width: 160, height: 16),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
UiShimmerLine(width: 80, height: 12),
|
||||
],
|
||||
),
|
||||
UiShimmerBox(
|
||||
width: 48,
|
||||
height: 28,
|
||||
borderRadius: UiConstants.radiusFull,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
// 3 time-slot rows (morning, afternoon, evening)
|
||||
..._buildSlotPlaceholders(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Generates 3 time-slot shimmer rows.
|
||||
List<Widget> _buildSlotPlaceholders() {
|
||||
return List.generate(3, (index) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: index < 2 ? UiConstants.space3 : 0,
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Icon placeholder
|
||||
UiShimmerBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius:
|
||||
UiConstants.radiusLg,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
// Text lines
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
UiShimmerLine(width: 80, height: 14),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
UiShimmerLine(width: 120, height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Checkbox circle
|
||||
const UiShimmerCircle(size: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export 'availability_page_skeleton.dart';
|
||||
export 'day_availability_skeleton.dart';
|
||||
export 'info_card_skeleton.dart';
|
||||
export 'quick_set_skeleton.dart';
|
||||
export 'week_navigation_skeleton.dart';
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for the info card at the bottom (icon + two text lines).
|
||||
class InfoCardSkeleton extends StatelessWidget {
|
||||
/// Creates an [InfoCardSkeleton].
|
||||
const InfoCardSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const UiShimmerCircle(size: 20),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const [
|
||||
UiShimmerLine(width: 140, height: 14),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
UiShimmerLine(height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for the quick-set section (title + 4 action buttons).
|
||||
class QuickSetSkeleton extends StatelessWidget {
|
||||
/// Creates a [QuickSetSkeleton].
|
||||
const QuickSetSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Title line
|
||||
const UiShimmerLine(width: 100, height: 14),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
// Row of 4 button placeholders
|
||||
Row(
|
||||
children: List.generate(4, (index) {
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: index == 0 ? 0 : UiConstants.space1,
|
||||
right: index == 3 ? 0 : UiConstants.space1,
|
||||
),
|
||||
child: UiShimmerBox(
|
||||
width: double.infinity,
|
||||
height: 32,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for the week navigation card (month header + 7 day cells).
|
||||
class WeekNavigationSkeleton extends StatelessWidget {
|
||||
/// Creates a [WeekNavigationSkeleton].
|
||||
const WeekNavigationSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Navigation header: left arrow, month label, right arrow
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: UiConstants.space4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const UiShimmerCircle(size: 32),
|
||||
UiShimmerLine(width: 140, height: 16),
|
||||
const UiShimmerCircle(size: 32),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 7 day cells
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.generate(7, (_) {
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space1),
|
||||
child: UiShimmerBox(
|
||||
width: double.infinity,
|
||||
height: 64,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,12 +17,12 @@ class StaffAvailabilityModule extends Module {
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
// Repository
|
||||
i.add<AvailabilityRepository>(AvailabilityRepositoryImpl.new);
|
||||
i.addLazySingleton<AvailabilityRepository>(AvailabilityRepositoryImpl.new);
|
||||
|
||||
// UseCases
|
||||
i.add(GetWeeklyAvailabilityUseCase.new);
|
||||
i.add(UpdateDayAvailabilityUseCase.new);
|
||||
i.add(ApplyQuickSetUseCase.new);
|
||||
i.addLazySingleton(GetWeeklyAvailabilityUseCase.new);
|
||||
i.addLazySingleton(UpdateDayAvailabilityUseCase.new);
|
||||
i.addLazySingleton(ApplyQuickSetUseCase.new);
|
||||
|
||||
// BLoC
|
||||
i.add(AvailabilityBloc.new);
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
import '../bloc/clock_in_bloc.dart';
|
||||
import '../bloc/clock_in_event.dart';
|
||||
import '../bloc/clock_in_state.dart';
|
||||
import '../widgets/clock_in_page_skeleton/clock_in_page_skeleton.dart';
|
||||
import '../widgets/commute_tracker.dart';
|
||||
import '../widgets/date_selector.dart';
|
||||
import '../widgets/lunch_break_modal.dart';
|
||||
@@ -52,8 +53,9 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
builder: (BuildContext context, ClockInState state) {
|
||||
if (state.status == ClockInStatus.loading &&
|
||||
state.todayShifts.isEmpty) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
return Scaffold(
|
||||
appBar: UiAppBar(title: i18n.title, showBackButton: false),
|
||||
body: const SafeArea(child: ClockInPageSkeleton()),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for the "Your Activity" section header text.
|
||||
class ActivityHeaderSkeleton extends StatelessWidget {
|
||||
/// Creates a shimmer line matching the activity header.
|
||||
const ActivityHeaderSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: UiShimmerLine(width: 120, height: 18),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'activity_header_skeleton.dart';
|
||||
import 'date_selector_skeleton.dart';
|
||||
import 'shift_card_skeleton.dart';
|
||||
import 'swipe_action_skeleton.dart';
|
||||
|
||||
/// Full-page shimmer skeleton shown while clock-in data loads.
|
||||
///
|
||||
/// Mirrors the loaded [ClockInPage] layout: date selector, activity header,
|
||||
/// two shift cards, and the swipe-to-check-in bar.
|
||||
class ClockInPageSkeleton extends StatelessWidget {
|
||||
/// Creates the clock-in page shimmer skeleton.
|
||||
const ClockInPageSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiShimmer(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: UiConstants.space24,
|
||||
top: UiConstants.space6,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const <Widget>[
|
||||
// Date selector row
|
||||
DateSelectorSkeleton(),
|
||||
SizedBox(height: UiConstants.space5),
|
||||
|
||||
// "Your Activity" header
|
||||
ActivityHeaderSkeleton(),
|
||||
SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Shift cards (show two placeholders)
|
||||
ShiftCardSkeleton(),
|
||||
ShiftCardSkeleton(),
|
||||
|
||||
// Swipe action bar
|
||||
SwipeActionSkeleton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for the [DateSelector] row of 7 day chips.
|
||||
class DateSelectorSkeleton extends StatelessWidget {
|
||||
/// Creates a shimmer placeholder matching the date selector layout.
|
||||
const DateSelectorSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 80,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List<Widget>.generate(7, (int index) {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space1,
|
||||
),
|
||||
child: UiShimmerBox(
|
||||
width: double.infinity,
|
||||
height: 80,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for a single shift info card.
|
||||
///
|
||||
/// Mirrors the two-column layout: left side has badge, title, and subtitle
|
||||
/// lines; right side has time range and rate lines.
|
||||
class ShiftCardSkeleton extends StatelessWidget {
|
||||
/// Creates a shimmer placeholder for one shift card.
|
||||
const ShiftCardSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: const Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// Left column: badge + title + subtitle
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
UiShimmerLine(width: 80, height: 10),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
UiShimmerLine(width: 160, height: 14),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
UiShimmerLine(width: 200, height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: UiConstants.space3),
|
||||
// Right column: time + rate
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
UiShimmerLine(width: 100, height: 12),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
UiShimmerLine(width: 60, height: 12),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for the swipe-to-check-in action area.
|
||||
class SwipeActionSkeleton extends StatelessWidget {
|
||||
/// Creates a shimmer placeholder matching the swipe bar height.
|
||||
const SwipeActionSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiShimmerBox(
|
||||
width: double.infinity,
|
||||
height: 60,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,9 @@ import 'package:staff_home/src/presentation/blocs/home/home_cubit.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/home_page/benefits_section.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/home_page/full_width_divider.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/home_page/home_header.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/home_page/staff_home_header_skeleton.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/home_page/placeholder_banner.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/home_page/home_page_skeleton.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/home_page/quick_actions_section.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/home_page/recommended_shifts_section.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/home_page/todays_shifts_section.dart';
|
||||
@@ -47,8 +49,13 @@ class WorkerHomePage extends StatelessWidget {
|
||||
children: [
|
||||
BlocBuilder<HomeCubit, HomeState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.staffName != current.staffName,
|
||||
previous.staffName != current.staffName ||
|
||||
previous.status != current.status,
|
||||
builder: (context, state) {
|
||||
if (state.status == HomeStatus.initial ||
|
||||
state.status == HomeStatus.loading) {
|
||||
return const StaffHomeHeaderSkeleton();
|
||||
}
|
||||
return HomeHeader(userName: state.staffName);
|
||||
},
|
||||
),
|
||||
@@ -59,8 +66,14 @@ class WorkerHomePage extends StatelessWidget {
|
||||
),
|
||||
child: BlocBuilder<HomeCubit, HomeState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.status != current.status ||
|
||||
previous.isProfileComplete != current.isProfileComplete,
|
||||
builder: (context, state) {
|
||||
if (state.status == HomeStatus.loading ||
|
||||
state.status == HomeStatus.initial) {
|
||||
return const HomePageSkeleton();
|
||||
}
|
||||
|
||||
if (!state.isProfileComplete) {
|
||||
return SizedBox(
|
||||
height: MediaQuery.of(context).size.height - 300,
|
||||
|
||||
@@ -11,16 +11,16 @@ class FullWidthDivider extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
//final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: UiConstants.space10),
|
||||
Transform.translate(
|
||||
offset: const Offset(-UiConstants.space4, 0),
|
||||
child: SizedBox(width: screenWidth, child: const Divider()),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space10),
|
||||
// Transform.translate(
|
||||
// offset: const Offset(-UiConstants.space4, 0),
|
||||
// child: SizedBox(width: screenWidth, child: const Divider()),
|
||||
// ),
|
||||
// const SizedBox(height: UiConstants.space10),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export 'home_page_skeleton/index.dart';
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'quick_actions_skeleton.dart';
|
||||
import 'recommended_section_skeleton.dart';
|
||||
import 'shift_section_skeleton.dart';
|
||||
import 'skeleton_divider.dart';
|
||||
|
||||
/// Shimmer loading skeleton for the staff home page.
|
||||
///
|
||||
/// Mimics the loaded layout with quick actions, today's shifts, tomorrow's
|
||||
/// shifts, recommended shifts, and benefits sections. Displayed while
|
||||
/// [HomeCubit] is fetching initial data.
|
||||
class HomePageSkeleton extends StatelessWidget {
|
||||
/// Creates a [HomePageSkeleton].
|
||||
const HomePageSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiShimmer(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Quick actions row (3 circular icons + labels)
|
||||
const QuickActionsSkeleton(),
|
||||
|
||||
const SkeletonDivider(),
|
||||
|
||||
// Today's Shifts section
|
||||
const ShiftSectionSkeleton(),
|
||||
|
||||
const SkeletonDivider(),
|
||||
|
||||
// Tomorrow's Shifts section
|
||||
const ShiftSectionSkeleton(),
|
||||
|
||||
const SkeletonDivider(),
|
||||
|
||||
// Recommended Shifts (horizontal cards)
|
||||
const RecommendedSectionSkeleton(),
|
||||
|
||||
const SkeletonDivider(),
|
||||
|
||||
// Benefits section
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: UiConstants.space3,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const UiShimmerSectionHeader(),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
UiShimmerList(
|
||||
itemCount: 2,
|
||||
itemBuilder: (index) => const UiShimmerListItem(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export 'home_page_skeleton.dart';
|
||||
export 'quick_actions_skeleton.dart';
|
||||
export 'recommended_section_skeleton.dart';
|
||||
export 'shift_card_skeleton.dart';
|
||||
export 'shift_section_skeleton.dart';
|
||||
export 'skeleton_divider.dart';
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Skeleton for the quick actions row (3 circular placeholders with labels).
|
||||
class QuickActionsSkeleton extends StatelessWidget {
|
||||
/// Creates a [QuickActionsSkeleton].
|
||||
const QuickActionsSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: UiConstants.space3,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.generate(3, (index) {
|
||||
return const Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
UiShimmerCircle(size: 48),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
UiShimmerLine(width: 60, height: 12),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Skeleton for the recommended shifts horizontal scroll section.
|
||||
class RecommendedSectionSkeleton extends StatelessWidget {
|
||||
/// Creates a [RecommendedSectionSkeleton].
|
||||
const RecommendedSectionSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: UiConstants.space3),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: UiConstants.space4),
|
||||
child: UiShimmerSectionHeader(),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
SizedBox(
|
||||
height: 120,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
),
|
||||
itemCount: 3,
|
||||
itemBuilder: (context, index) => Padding(
|
||||
padding: const EdgeInsets.only(right: UiConstants.space3),
|
||||
child: UiShimmerBox(
|
||||
width: 200,
|
||||
height: 120,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Skeleton for a single compact shift card on the home page.
|
||||
class ShiftCardSkeleton extends StatelessWidget {
|
||||
/// Creates a [ShiftCardSkeleton].
|
||||
const ShiftCardSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: UiColors.border),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
UiShimmerBox(width: 48, height: 48),
|
||||
SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
UiShimmerLine(width: 160, height: 14),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
UiShimmerLine(width: 120, height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: UiConstants.space3),
|
||||
UiShimmerBox(width: 56, height: 24),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'shift_card_skeleton.dart';
|
||||
|
||||
/// Skeleton for a shift section (section header + 2 shift card placeholders).
|
||||
class ShiftSectionSkeleton extends StatelessWidget {
|
||||
/// Creates a [ShiftSectionSkeleton].
|
||||
const ShiftSectionSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: UiConstants.space3,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const UiShimmerSectionHeader(),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
UiShimmerList(
|
||||
itemCount: 2,
|
||||
itemBuilder: (index) => const ShiftCardSkeleton(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A thin full-width divider placeholder matching the home page layout.
|
||||
class SkeletonDivider extends StatelessWidget {
|
||||
/// Creates a [SkeletonDivider].
|
||||
const SkeletonDivider({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Divider(height: 1, thickness: 0.5, color: UiColors.border);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for the staff home header during loading.
|
||||
///
|
||||
/// Mimics the avatar circle, welcome text, and user name layout.
|
||||
class StaffHomeHeaderSkeleton extends StatelessWidget {
|
||||
/// Creates a [StaffHomeHeaderSkeleton].
|
||||
const StaffHomeHeaderSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiShimmer(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
UiConstants.space4,
|
||||
UiConstants.space4,
|
||||
UiConstants.space4,
|
||||
UiConstants.space3,
|
||||
),
|
||||
child: Row(
|
||||
spacing: UiConstants.space3,
|
||||
children: const <Widget>[
|
||||
UiShimmerCircle(size: UiConstants.space12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
UiShimmerLine(width: 80, height: 12),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
UiShimmerLine(width: 120, height: 16),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -35,15 +35,7 @@ class TodaysShiftsSection extends StatelessWidget {
|
||||
)
|
||||
: null,
|
||||
child: state.status == HomeStatus.loading
|
||||
? const Center(
|
||||
child: SizedBox(
|
||||
height: UiConstants.space10,
|
||||
width: UiConstants.space10,
|
||||
child: CircularProgressIndicator(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
),
|
||||
)
|
||||
? const _ShiftsSectionSkeleton()
|
||||
: shifts.isEmpty
|
||||
? EmptyStateWidget(
|
||||
message: emptyI18n.no_shifts_today,
|
||||
@@ -66,3 +58,40 @@ class TodaysShiftsSection extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Inline shimmer skeleton for the shifts section loading state.
|
||||
class _ShiftsSectionSkeleton extends StatelessWidget {
|
||||
const _ShiftsSectionSkeleton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiShimmer(
|
||||
child: UiShimmerList(
|
||||
itemCount: 2,
|
||||
itemBuilder: (index) => Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: UiColors.border),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
UiShimmerBox(width: 48, height: 48),
|
||||
SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
UiShimmerLine(width: 160, height: 14),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
UiShimmerLine(width: 120, height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class StaffHomeModule extends Module {
|
||||
);
|
||||
|
||||
// Presentation layer - Cubits
|
||||
i.addSingleton(
|
||||
i.addLazySingleton(
|
||||
() => HomeCubit(
|
||||
repository: i.get<HomeRepository>(),
|
||||
getProfileCompletion: i.get<GetProfileCompletionUseCase>(),
|
||||
|
||||
@@ -67,6 +67,16 @@ class PaymentsRepositoryImpl
|
||||
.execute();
|
||||
|
||||
return response.data.recentPayments.map((dc.ListRecentPaymentsByStaffIdRecentPayments payment) {
|
||||
// Extract shift details from nested application structure
|
||||
final String? shiftTitle = payment.application.shiftRole.shift.title;
|
||||
final String? locationAddress = payment.application.shiftRole.shift.locationAddress;
|
||||
final double? hoursWorked = payment.application.shiftRole.hours;
|
||||
final double? hourlyRate = payment.application.shiftRole.role.costPerHour;
|
||||
// Extract hub details from order
|
||||
final String? locationHub = payment.invoice.order.teamHub.hubName;
|
||||
final String? hubAddress = payment.invoice.order.teamHub.address;
|
||||
final String? shiftLocation = locationAddress ?? hubAddress;
|
||||
|
||||
return StaffPayment(
|
||||
id: payment.id,
|
||||
staffId: payment.staffId,
|
||||
@@ -74,6 +84,12 @@ class PaymentsRepositoryImpl
|
||||
amount: payment.invoice.amount,
|
||||
status: PaymentAdapter.toPaymentStatus(payment.status?.stringValue ?? 'UNKNOWN'),
|
||||
paidAt: _service.toDateTime(payment.invoice.issueDate),
|
||||
shiftTitle: shiftTitle,
|
||||
shiftLocation: locationHub,
|
||||
locationAddress: shiftLocation,
|
||||
hoursWorked: hoursWorked,
|
||||
hourlyRate: hourlyRate,
|
||||
workedTime: payment.workedTime,
|
||||
);
|
||||
}).toList();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
@@ -9,8 +8,8 @@ import 'package:core_localization/core_localization.dart';
|
||||
import '../blocs/payments/payments_bloc.dart';
|
||||
import '../blocs/payments/payments_event.dart';
|
||||
import '../blocs/payments/payments_state.dart';
|
||||
import '../widgets/payments_page_skeleton.dart';
|
||||
import '../widgets/payment_stats_card.dart';
|
||||
import '../widgets/pending_pay_card.dart';
|
||||
import '../widgets/payment_history_item.dart';
|
||||
import '../widgets/earnings_graph.dart';
|
||||
|
||||
@@ -43,7 +42,7 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
},
|
||||
builder: (BuildContext context, PaymentsState state) {
|
||||
if (state is PaymentsLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
return const PaymentsPageSkeleton();
|
||||
|
||||
} else if (state is PaymentsError) {
|
||||
return Center(
|
||||
@@ -172,17 +171,7 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Pending Pay
|
||||
if (state.summary.pendingEarnings > 0)
|
||||
PendingPayCard(
|
||||
amount: state.summary.pendingEarnings,
|
||||
onCashOut: () {
|
||||
Modular.to.pushNamed('${StaffPaths.payments}early-pay');
|
||||
},
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
|
||||
// Recent Payments
|
||||
if (state.history.isNotEmpty)
|
||||
@@ -191,7 +180,7 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"Recent Payments",
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
style: UiTypography.body1b,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Column(
|
||||
@@ -201,16 +190,16 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
bottom: UiConstants.space2),
|
||||
child: PaymentHistoryItem(
|
||||
amount: payment.amount,
|
||||
title: "Shift Payment",
|
||||
location: "Varies",
|
||||
address: "Payment ID: ${payment.id}",
|
||||
title: payment.shiftTitle ?? "Shift Payment",
|
||||
location: payment.shiftLocation ?? "Varies",
|
||||
address: payment.locationAddress ?? payment.id,
|
||||
date: payment.paidAt != null
|
||||
? DateFormat('E, MMM d')
|
||||
.format(payment.paidAt!)
|
||||
: 'Pending',
|
||||
workedTime: "Completed",
|
||||
hours: 0,
|
||||
rate: 0.0,
|
||||
workedTime: payment.workedTime ?? "Completed",
|
||||
hours: (payment.hoursWorked ?? 0).toInt(),
|
||||
rate: payment.hourlyRate ?? 0.0,
|
||||
status: payment.status.name.toUpperCase(),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -32,13 +32,7 @@ class PaymentHistoryItem extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
border: Border.all(color: UiColors.border, width: 0.5),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -77,7 +71,7 @@ class PaymentHistoryItem extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
child: const Icon(
|
||||
UiIcons.chart,
|
||||
UiIcons.dollar,
|
||||
color: UiColors.mutedForeground,
|
||||
size: 24,
|
||||
),
|
||||
@@ -98,7 +92,7 @@ class PaymentHistoryItem extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
title,
|
||||
style: UiTypography.body2b.textPrimary,
|
||||
style: UiTypography.body2m,
|
||||
),
|
||||
Text(
|
||||
location,
|
||||
@@ -112,7 +106,7 @@ class PaymentHistoryItem extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"\$${amount.toStringAsFixed(0)}",
|
||||
style: UiTypography.headline4m.textPrimary,
|
||||
style: UiTypography.headline4b,
|
||||
),
|
||||
Text(
|
||||
"\$${rate.toStringAsFixed(0)}/hr · ${hours}h",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export 'payments_page_skeleton/index.dart';
|
||||
@@ -0,0 +1,2 @@
|
||||
export 'payment_item_skeleton.dart';
|
||||
export 'payments_page_skeleton.dart';
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Skeleton for a single payment history item.
|
||||
///
|
||||
/// Matches the [PaymentHistoryItem] layout with a leading icon, title/subtitle
|
||||
/// lines, and trailing amount text.
|
||||
class PaymentItemSkeleton extends StatelessWidget {
|
||||
/// Creates a [PaymentItemSkeleton].
|
||||
const PaymentItemSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: UiColors.border),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
UiShimmerCircle(size: 40),
|
||||
SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
UiShimmerLine(width: 140, height: 14),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
UiShimmerLine(width: 100, height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: UiConstants.space3),
|
||||
UiShimmerLine(width: 60, height: 16),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'payment_item_skeleton.dart';
|
||||
|
||||
/// Shimmer loading skeleton for the payments page.
|
||||
///
|
||||
/// Mimics the loaded layout: a gradient header with balance and period tabs,
|
||||
/// an earnings graph placeholder, stat cards, and a recent payments list.
|
||||
class PaymentsPageSkeleton extends StatelessWidget {
|
||||
/// Creates a [PaymentsPageSkeleton].
|
||||
const PaymentsPageSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiShimmer(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
// Header section with gradient
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
UiColors.primary,
|
||||
UiColors.primary.withValues(alpha: 0.8),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
UiConstants.space5,
|
||||
MediaQuery.of(context).padding.top + UiConstants.space6,
|
||||
UiConstants.space5,
|
||||
UiConstants.space8,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Title placeholder
|
||||
const UiShimmerLine(width: 120, height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
// Balance center
|
||||
const Center(
|
||||
child: Column(
|
||||
children: [
|
||||
UiShimmerLine(width: 100, height: 14),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
UiShimmerLine(width: 160, height: 36),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Period tabs placeholder
|
||||
UiShimmerBox(
|
||||
width: double.infinity,
|
||||
height: 40,
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Main content offset upwards
|
||||
Transform.translate(
|
||||
offset: const Offset(0, -UiConstants.space4),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Earnings graph placeholder
|
||||
UiShimmerBox(
|
||||
width: double.infinity,
|
||||
height: 180,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
// Quick stats row
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: UiShimmerStatsCard()),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(child: UiShimmerStatsCard()),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
|
||||
// Recent Payments header
|
||||
const UiShimmerSectionHeader(),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
|
||||
// Payment history items
|
||||
UiShimmerList(
|
||||
itemCount: 4,
|
||||
itemBuilder: (index) => const PaymentItemSkeleton(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import '../blocs/profile_cubit.dart';
|
||||
import '../blocs/profile_state.dart';
|
||||
import '../widgets/logout_button.dart';
|
||||
import '../widgets/header/profile_header.dart';
|
||||
import '../widgets/profile_page_skeleton/profile_page_skeleton.dart';
|
||||
import '../widgets/reliability_score_bar.dart';
|
||||
import '../widgets/reliability_stats_card.dart';
|
||||
import '../widgets/sections/index.dart';
|
||||
@@ -63,9 +64,9 @@ class StaffProfilePage extends StatelessWidget {
|
||||
}
|
||||
},
|
||||
builder: (BuildContext context, ProfileState state) {
|
||||
// Show loading spinner if status is loading
|
||||
// Show shimmer skeleton while profile data loads
|
||||
if (state.status == ProfileStatus.loading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
return const ProfilePageSkeleton();
|
||||
}
|
||||
|
||||
if (state.status == ProfileStatus.error) {
|
||||
@@ -87,7 +88,7 @@ class StaffProfilePage extends StatelessWidget {
|
||||
|
||||
final Staff? profile = state.profile;
|
||||
if (profile == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
return const ProfilePageSkeleton();
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export 'menu_section_skeleton.dart';
|
||||
export 'profile_header_skeleton.dart';
|
||||
export 'profile_page_skeleton.dart';
|
||||
export 'reliability_score_skeleton.dart';
|
||||
export 'reliability_stats_skeleton.dart';
|
||||
@@ -0,0 +1,87 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for a profile menu section.
|
||||
///
|
||||
/// Mirrors the section layout: a section title line followed by a grid of
|
||||
/// square menu item placeholders. Reused for onboarding, compliance, finance,
|
||||
/// and support sections.
|
||||
class MenuSectionSkeleton extends StatelessWidget {
|
||||
/// Creates a [MenuSectionSkeleton].
|
||||
const MenuSectionSkeleton({
|
||||
super.key,
|
||||
this.itemCount = 4,
|
||||
this.crossAxisCount = 3,
|
||||
});
|
||||
|
||||
/// Number of menu item placeholders to display.
|
||||
final int itemCount;
|
||||
|
||||
/// Number of columns in the grid.
|
||||
final int crossAxisCount;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// Section title placeholder
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: UiConstants.space1),
|
||||
child: const UiShimmerLine(width: 100, height: 12),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
// Menu items grid
|
||||
LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
const double spacing = UiConstants.space3;
|
||||
final double totalWidth = constraints.maxWidth;
|
||||
final double totalSpacingWidth = spacing * (crossAxisCount - 1);
|
||||
final double itemWidth =
|
||||
(totalWidth - totalSpacingWidth) / crossAxisCount;
|
||||
|
||||
return Wrap(
|
||||
spacing: spacing,
|
||||
runSpacing: spacing,
|
||||
children: List<Widget>.generate(itemCount, (int index) {
|
||||
return SizedBox(
|
||||
width: itemWidth,
|
||||
child: const _MenuItemSkeleton(),
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Single menu item shimmer: a bordered square with an icon circle and label
|
||||
/// line.
|
||||
class _MenuItemSkeleton extends StatelessWidget {
|
||||
const _MenuItemSkeleton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
padding: const EdgeInsets.all(UiConstants.space2),
|
||||
child: const AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
UiShimmerBox(width: 36, height: 36),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
UiShimmerLine(width: 48, height: 10),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for the profile header section.
|
||||
///
|
||||
/// Mirrors [ProfileHeader] layout: circle avatar, name line, and level badge
|
||||
/// on the primary-colored background with rounded bottom corners.
|
||||
class ProfileHeaderSkeleton extends StatelessWidget {
|
||||
/// Creates a [ProfileHeaderSkeleton].
|
||||
const ProfileHeaderSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
UiConstants.space5,
|
||||
UiConstants.space5,
|
||||
UiConstants.space5,
|
||||
UiConstants.space16,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: UiColors.primary,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
bottom: Radius.circular(UiConstants.space6),
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
// Avatar placeholder
|
||||
const UiShimmerCircle(size: 112),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
// Name placeholder
|
||||
const UiShimmerLine(width: 160, height: 20),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
// Level badge placeholder
|
||||
const UiShimmerBox(width: 100, height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'menu_section_skeleton.dart';
|
||||
import 'profile_header_skeleton.dart';
|
||||
import 'reliability_score_skeleton.dart';
|
||||
import 'reliability_stats_skeleton.dart';
|
||||
|
||||
/// Full-page shimmer skeleton for [StaffProfilePage].
|
||||
///
|
||||
/// Mimics the loaded profile layout: header, reliability stats, score bar,
|
||||
/// and four menu sections. Displayed while [ProfileCubit] fetches data.
|
||||
class ProfilePageSkeleton extends StatelessWidget {
|
||||
/// Creates a [ProfilePageSkeleton].
|
||||
const ProfilePageSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiShimmer(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
// Header with avatar, name, and badge
|
||||
const ProfileHeaderSkeleton(),
|
||||
|
||||
// Content offset to overlap the header bottom radius
|
||||
Transform.translate(
|
||||
offset: const Offset(0, -UiConstants.space6),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
),
|
||||
child: Column(
|
||||
spacing: UiConstants.space6,
|
||||
children: const <Widget>[
|
||||
// Reliability stats row (5 items)
|
||||
ReliabilityStatsSkeleton(),
|
||||
|
||||
// Reliability score bar
|
||||
ReliabilityScoreSkeleton(),
|
||||
|
||||
// Onboarding section (4 items, 3 columns)
|
||||
MenuSectionSkeleton(itemCount: 4, crossAxisCount: 3),
|
||||
|
||||
// Compliance section (3 items, 3 columns)
|
||||
MenuSectionSkeleton(itemCount: 3, crossAxisCount: 3),
|
||||
|
||||
// Finance section (3 items, 3 columns)
|
||||
MenuSectionSkeleton(itemCount: 3, crossAxisCount: 3),
|
||||
|
||||
// Support section (2 items, 3 columns)
|
||||
MenuSectionSkeleton(itemCount: 2, crossAxisCount: 3),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for the reliability score bar section.
|
||||
///
|
||||
/// Mirrors [ReliabilityScoreBar] layout: a tinted container with a title line,
|
||||
/// percentage line, progress bar placeholder, and description line.
|
||||
class ReliabilityScoreSkeleton extends StatelessWidget {
|
||||
/// Creates a [ReliabilityScoreSkeleton].
|
||||
const ReliabilityScoreSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary.withValues(alpha: 0.1),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// Title row with label and percentage
|
||||
const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
UiShimmerLine(width: 120, height: 14),
|
||||
UiShimmerLine(width: 40, height: 18),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
// Progress bar placeholder
|
||||
UiShimmerBox(
|
||||
width: double.infinity,
|
||||
height: 8,
|
||||
borderRadius: UiConstants.radiusSm,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
// Description line
|
||||
const UiShimmerLine(width: 200, height: 10),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for the reliability stats card.
|
||||
///
|
||||
/// Mirrors [ReliabilityStatsCard] layout: a bordered card containing five
|
||||
/// evenly-spaced stat columns, each with an icon circle, value line, and
|
||||
/// label line.
|
||||
class ReliabilityStatsSkeleton extends StatelessWidget {
|
||||
/// Creates a [ReliabilityStatsSkeleton].
|
||||
const ReliabilityStatsSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
_StatItemSkeleton(),
|
||||
_StatItemSkeleton(),
|
||||
_StatItemSkeleton(),
|
||||
_StatItemSkeleton(),
|
||||
_StatItemSkeleton(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Single stat column shimmer: icon circle, value line, label line.
|
||||
class _StatItemSkeleton extends StatelessWidget {
|
||||
const _StatItemSkeleton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Expanded(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
UiShimmerBox(width: UiConstants.space10, height: UiConstants.space10),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
UiShimmerLine(width: 28, height: 14),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
UiShimmerLine(width: 36, height: 10),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ class StaffProfileModule extends Module {
|
||||
|
||||
// Presentation layer - Cubit as singleton to avoid recreation
|
||||
// BlocProvider will use this same instance, preventing state emission after close
|
||||
i.addSingleton<ProfileCubit>(
|
||||
i.addLazySingleton<ProfileCubit>(
|
||||
() => ProfileCubit(
|
||||
i.get<GetStaffProfileUseCase>(),
|
||||
i.get<SignOutStaffUseCase>(),
|
||||
|
||||
@@ -11,6 +11,7 @@ import '../blocs/certificates/certificates_state.dart';
|
||||
import '../widgets/add_certificate_card.dart';
|
||||
import '../widgets/certificate_card.dart';
|
||||
import '../widgets/certificates_header.dart';
|
||||
import '../widgets/certificates_skeleton/certificates_skeleton.dart';
|
||||
|
||||
/// Page for viewing and managing staff certificates.
|
||||
///
|
||||
@@ -28,9 +29,7 @@ class CertificatesPage extends StatelessWidget {
|
||||
builder: (BuildContext context, CertificatesState state) {
|
||||
if (state.status == CertificatesStatus.loading ||
|
||||
state.status == CertificatesStatus.initial) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
return const Scaffold(body: CertificatesSkeleton());
|
||||
}
|
||||
|
||||
if (state.status == CertificatesStatus.failure) {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for a single certificate card.
|
||||
class CertificateCardSkeleton extends StatelessWidget {
|
||||
/// Creates a [CertificateCardSkeleton].
|
||||
const CertificateCardSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: const Row(
|
||||
children: <Widget>[
|
||||
UiShimmerCircle(size: 40),
|
||||
SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
UiShimmerLine(width: 140, height: 14),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
UiShimmerLine(width: 100, height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
UiShimmerBox(width: 60, height: 28),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for the certificates progress header.
|
||||
class CertificatesHeaderSkeleton extends StatelessWidget {
|
||||
/// Creates a [CertificatesHeaderSkeleton].
|
||||
const CertificatesHeaderSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
decoration: const BoxDecoration(color: UiColors.primary),
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
const UiShimmerCircle(size: 64),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
UiShimmerLine(
|
||||
width: 120,
|
||||
height: 14,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
UiShimmerLine(
|
||||
width: 80,
|
||||
height: 12,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'certificate_card_skeleton.dart';
|
||||
import 'certificates_header_skeleton.dart';
|
||||
|
||||
/// Full-page shimmer skeleton shown while certificates are loading.
|
||||
class CertificatesSkeleton extends StatelessWidget {
|
||||
/// Creates a [CertificatesSkeleton].
|
||||
const CertificatesSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiShimmer(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
const CertificatesHeaderSkeleton(),
|
||||
Transform.translate(
|
||||
offset: const Offset(0, -UiConstants.space12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
),
|
||||
child: UiShimmerList(
|
||||
itemCount: 4,
|
||||
spacing: UiConstants.space3,
|
||||
itemBuilder: (int index) =>
|
||||
const CertificateCardSkeleton(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import '../blocs/documents/documents_cubit.dart';
|
||||
import '../blocs/documents/documents_state.dart';
|
||||
import '../widgets/document_card.dart';
|
||||
import '../widgets/documents_progress_card.dart';
|
||||
import '../widgets/documents_skeleton/documents_skeleton.dart';
|
||||
|
||||
class DocumentsPage extends StatelessWidget {
|
||||
const DocumentsPage({super.key});
|
||||
@@ -28,11 +29,7 @@ class DocumentsPage extends StatelessWidget {
|
||||
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (BuildContext context, DocumentsState state) {
|
||||
if (state.status == DocumentsStatus.loading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(UiColors.primary),
|
||||
),
|
||||
);
|
||||
return const DocumentsSkeleton();
|
||||
}
|
||||
if (state.status == DocumentsStatus.failure) {
|
||||
return Center(
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for a single document card row.
|
||||
class DocumentCardSkeleton extends StatelessWidget {
|
||||
/// Creates a [DocumentCardSkeleton].
|
||||
const DocumentCardSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: UiColors.border),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
color: UiColors.cardViewBackground,
|
||||
),
|
||||
child: const Row(
|
||||
children: <Widget>[
|
||||
UiShimmerCircle(size: 40),
|
||||
SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
UiShimmerLine(width: 160, height: 14),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
UiShimmerLine(width: 100, height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
UiShimmerBox(width: 24, height: 24),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for the documents progress card.
|
||||
class DocumentsProgressSkeleton extends StatelessWidget {
|
||||
/// Creates a [DocumentsProgressSkeleton].
|
||||
const DocumentsProgressSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: UiColors.border),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
color: UiColors.cardViewBackground,
|
||||
),
|
||||
child: const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
UiShimmerLine(width: 140, height: 14),
|
||||
SizedBox(height: UiConstants.space3),
|
||||
UiShimmerBox(width: double.infinity, height: 8),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
UiShimmerLine(width: 80, height: 12),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'document_card_skeleton.dart';
|
||||
import 'documents_progress_skeleton.dart';
|
||||
|
||||
/// Full-page shimmer skeleton shown while documents are loading.
|
||||
class DocumentsSkeleton extends StatelessWidget {
|
||||
/// Creates a [DocumentsSkeleton].
|
||||
const DocumentsSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiShimmer(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
vertical: UiConstants.space6,
|
||||
),
|
||||
children: <Widget>[
|
||||
const DocumentsProgressSkeleton(),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
UiShimmerList(
|
||||
itemCount: 5,
|
||||
spacing: UiConstants.space3,
|
||||
itemBuilder: (int index) => const DocumentCardSkeleton(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
import '../blocs/tax_forms/tax_forms_cubit.dart';
|
||||
import '../blocs/tax_forms/tax_forms_state.dart';
|
||||
import '../widgets/tax_forms_page/index.dart';
|
||||
import '../widgets/tax_forms_skeleton/tax_forms_skeleton.dart';
|
||||
|
||||
class TaxFormsPage extends StatelessWidget {
|
||||
const TaxFormsPage({super.key});
|
||||
@@ -31,7 +32,7 @@ class TaxFormsPage extends StatelessWidget {
|
||||
child: BlocBuilder<TaxFormsCubit, TaxFormsState>(
|
||||
builder: (BuildContext context, TaxFormsState state) {
|
||||
if (state.status == TaxFormsStatus.loading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
return const TaxFormsSkeleton();
|
||||
}
|
||||
|
||||
if (state.status == TaxFormsStatus.failure) {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for a single tax form card.
|
||||
class TaxFormCardSkeleton extends StatelessWidget {
|
||||
/// Creates a [TaxFormCardSkeleton].
|
||||
const TaxFormCardSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: UiColors.border),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
color: UiColors.cardViewBackground,
|
||||
),
|
||||
child: const Row(
|
||||
children: <Widget>[
|
||||
UiShimmerCircle(size: 40),
|
||||
SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
UiShimmerLine(width: 120, height: 14),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
UiShimmerLine(width: 80, height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
UiShimmerBox(width: 60, height: 24),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'tax_form_card_skeleton.dart';
|
||||
|
||||
/// Full-page shimmer skeleton shown while tax forms are loading.
|
||||
class TaxFormsSkeleton extends StatelessWidget {
|
||||
/// Creates a [TaxFormsSkeleton].
|
||||
const TaxFormsSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiShimmer(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
vertical: UiConstants.space6,
|
||||
),
|
||||
child: Column(
|
||||
spacing: UiConstants.space4,
|
||||
children: <Widget>[
|
||||
// Info card placeholder
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: UiColors.border),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
color: UiColors.cardViewBackground,
|
||||
),
|
||||
child: const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
UiShimmerLine(width: 180, height: 14),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
UiShimmerLine(height: 12),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
UiShimmerLine(width: 200, height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Progress overview placeholder
|
||||
const UiShimmerStatsCard(),
|
||||
// Form card placeholders
|
||||
UiShimmerList(
|
||||
itemCount: 3,
|
||||
spacing: UiConstants.space2,
|
||||
itemBuilder: (int index) => const TaxFormCardSkeleton(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import '../blocs/bank_account_cubit.dart';
|
||||
import '../blocs/bank_account_state.dart';
|
||||
import '../widgets/account_card.dart';
|
||||
import '../widgets/add_account_form.dart';
|
||||
import '../widgets/bank_account_skeleton/bank_account_skeleton.dart';
|
||||
import '../widgets/security_notice.dart';
|
||||
|
||||
class BankAccountPage extends StatelessWidget {
|
||||
@@ -49,7 +50,7 @@ class BankAccountPage extends StatelessWidget {
|
||||
builder: (BuildContext context, BankAccountState state) {
|
||||
if (state.status == BankAccountStatus.loading &&
|
||||
state.accounts.isEmpty) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
return const BankAccountSkeleton();
|
||||
}
|
||||
|
||||
if (state.status == BankAccountStatus.error) {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for a single bank account card.
|
||||
class AccountCardSkeleton extends StatelessWidget {
|
||||
/// Creates an [AccountCardSkeleton].
|
||||
const AccountCardSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: UiColors.border),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
color: UiColors.cardViewBackground,
|
||||
),
|
||||
child: const Row(
|
||||
children: <Widget>[
|
||||
UiShimmerCircle(size: 40),
|
||||
SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
UiShimmerLine(width: 140, height: 14),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
UiShimmerLine(width: 100, height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
UiShimmerBox(width: 48, height: 24),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'account_card_skeleton.dart';
|
||||
import 'security_notice_skeleton.dart';
|
||||
|
||||
/// Full-page shimmer skeleton shown while bank accounts are loading.
|
||||
class BankAccountSkeleton extends StatelessWidget {
|
||||
/// Creates a [BankAccountSkeleton].
|
||||
const BankAccountSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiShimmer(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const SecurityNoticeSkeleton(),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
UiShimmerList(
|
||||
itemCount: 2,
|
||||
spacing: UiConstants.space3,
|
||||
itemBuilder: (int index) => const AccountCardSkeleton(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for the security notice banner.
|
||||
class SecurityNoticeSkeleton extends StatelessWidget {
|
||||
/// Creates a [SecurityNoticeSkeleton].
|
||||
const SecurityNoticeSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: UiColors.border),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
color: UiColors.cardViewBackground,
|
||||
),
|
||||
child: const Row(
|
||||
children: <Widget>[
|
||||
UiShimmerCircle(size: 24),
|
||||
SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
UiShimmerLine(width: 160, height: 14),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
UiShimmerLine(height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import 'package:krow_core/core.dart';
|
||||
import '../blocs/time_card_bloc.dart';
|
||||
import '../widgets/month_selector.dart';
|
||||
import '../widgets/shift_history_list.dart';
|
||||
import '../widgets/time_card_skeleton/time_card_skeleton.dart';
|
||||
import '../widgets/time_card_summary.dart';
|
||||
|
||||
/// The main page for displaying the staff time card.
|
||||
@@ -50,7 +51,7 @@ class _TimeCardPageState extends State<TimeCardPage> {
|
||||
},
|
||||
builder: (BuildContext context, TimeCardState state) {
|
||||
if (state is TimeCardLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
return const TimeCardSkeleton();
|
||||
} else if (state is TimeCardError) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for the month selector row.
|
||||
class MonthSelectorSkeleton extends StatelessWidget {
|
||||
/// Creates a [MonthSelectorSkeleton].
|
||||
const MonthSelectorSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
UiShimmerCircle(size: 32),
|
||||
UiShimmerLine(width: 120, height: 16),
|
||||
UiShimmerCircle(size: 32),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for a single shift history row.
|
||||
class ShiftHistorySkeleton extends StatelessWidget {
|
||||
/// Creates a [ShiftHistorySkeleton].
|
||||
const ShiftHistorySkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: UiColors.border),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
color: UiColors.cardViewBackground,
|
||||
),
|
||||
child: const Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
UiShimmerLine(width: 140, height: 14),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
UiShimmerLine(width: 100, height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
UiShimmerLine(width: 60, height: 14),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
UiShimmerLine(width: 40, height: 12),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'month_selector_skeleton.dart';
|
||||
import 'shift_history_skeleton.dart';
|
||||
import 'time_card_summary_skeleton.dart';
|
||||
|
||||
/// Full-page shimmer skeleton shown while time card data is loading.
|
||||
class TimeCardSkeleton extends StatelessWidget {
|
||||
/// Creates a [TimeCardSkeleton].
|
||||
const TimeCardSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiShimmer(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
vertical: UiConstants.space6,
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
const MonthSelectorSkeleton(),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
const TimeCardSummarySkeleton(),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
UiShimmerList(
|
||||
itemCount: 5,
|
||||
spacing: UiConstants.space3,
|
||||
itemBuilder: (int index) => const ShiftHistorySkeleton(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for the time card summary (hours + earnings).
|
||||
class TimeCardSummarySkeleton extends StatelessWidget {
|
||||
/// Creates a [TimeCardSummarySkeleton].
|
||||
const TimeCardSummarySkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Row(
|
||||
children: <Widget>[
|
||||
Expanded(child: UiShimmerStatsCard()),
|
||||
SizedBox(width: UiConstants.space3),
|
||||
Expanded(child: UiShimmerStatsCard()),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,24 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:staff_attire/src/presentation/blocs/attire/attire_cubit.dart';
|
||||
import 'package:staff_attire/src/presentation/blocs/attire/attire_state.dart';
|
||||
|
||||
import '../widgets/attire_filter_chips.dart';
|
||||
import '../widgets/attire_empty_section.dart';
|
||||
import '../widgets/attire_info_card.dart';
|
||||
import '../widgets/attire_item_card.dart';
|
||||
import '../widgets/attire_section_header.dart';
|
||||
import '../widgets/attire_section_tab.dart';
|
||||
import '../widgets/attire_skeleton/attire_skeleton.dart';
|
||||
|
||||
class AttirePage extends StatelessWidget {
|
||||
class AttirePage extends StatefulWidget {
|
||||
const AttirePage({super.key});
|
||||
|
||||
@override
|
||||
State<AttirePage> createState() => _AttirePageState();
|
||||
}
|
||||
|
||||
class _AttirePageState extends State<AttirePage> {
|
||||
bool _showRequired = true;
|
||||
bool _showNonEssential = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final AttireCubit cubit = Modular.get<AttireCubit>();
|
||||
@@ -39,10 +50,15 @@ class AttirePage extends StatelessWidget {
|
||||
},
|
||||
builder: (BuildContext context, AttireState state) {
|
||||
if (state.status == AttireStatus.loading && state.options.isEmpty) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
return const AttireSkeleton();
|
||||
}
|
||||
|
||||
final List<AttireItem> filteredOptions = state.filteredOptions;
|
||||
final List<AttireItem> requiredItems = state.options
|
||||
.where((AttireItem item) => item.isMandatory)
|
||||
.toList();
|
||||
final List<AttireItem> nonEssentialItems = state.options
|
||||
.where((AttireItem item) => !item.isMandatory)
|
||||
.toList();
|
||||
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
@@ -55,55 +71,110 @@ class AttirePage extends StatelessWidget {
|
||||
const AttireInfoCard(),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
// Filter Chips
|
||||
AttireFilterChips(
|
||||
selectedFilter: state.filter,
|
||||
onFilterChanged: cubit.updateFilter,
|
||||
// Section toggle chips
|
||||
Row(
|
||||
children: <Widget>[
|
||||
AttireSectionTab(
|
||||
label: 'Required',
|
||||
isSelected: _showRequired,
|
||||
onTap: () => setState(
|
||||
() => _showRequired = !_showRequired,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
AttireSectionTab(
|
||||
label: 'Non-Essential',
|
||||
isSelected: _showNonEssential,
|
||||
onTap: () => setState(
|
||||
() => _showNonEssential = !_showNonEssential,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
// Item List
|
||||
if (filteredOptions.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space10,
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.shirt,
|
||||
size: 48,
|
||||
color: UiColors.iconInactive,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
context.t.staff_profile_attire.capture.no_items_filter,
|
||||
style: UiTypography.body1m.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
// Required section
|
||||
if (_showRequired) ...<Widget>[
|
||||
AttireSectionHeader(
|
||||
title: 'Required',
|
||||
count: requiredItems.length,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
if (requiredItems.isEmpty)
|
||||
AttireEmptySection(
|
||||
message: context
|
||||
.t
|
||||
.staff_profile_attire
|
||||
.capture
|
||||
.no_items_filter,
|
||||
)
|
||||
else
|
||||
...requiredItems.map((AttireItem item) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: UiConstants.space3,
|
||||
),
|
||||
child: AttireItemCard(
|
||||
item: item,
|
||||
isUploading: false,
|
||||
uploadedPhotoUrl: state.photoUrls[item.id],
|
||||
onTap: () {
|
||||
Modular.to.toAttireCapture(
|
||||
item: item,
|
||||
initialPhotoUrl: state.photoUrls[item.id],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
|
||||
// Divider between sections
|
||||
if (_showRequired && _showNonEssential)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space8,
|
||||
),
|
||||
child: Divider(),
|
||||
)
|
||||
else
|
||||
...filteredOptions.map((AttireItem item) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: UiConstants.space3,
|
||||
),
|
||||
child: AttireItemCard(
|
||||
item: item,
|
||||
isUploading: false,
|
||||
uploadedPhotoUrl: state.photoUrls[item.id],
|
||||
onTap: () {
|
||||
Modular.to.toAttireCapture(
|
||||
item: item,
|
||||
initialPhotoUrl: state.photoUrls[item.id],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
// Non-Essential section
|
||||
if (_showNonEssential) ...<Widget>[
|
||||
AttireSectionHeader(
|
||||
title: 'Non-Essential',
|
||||
count: nonEssentialItems.length,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
if (nonEssentialItems.isEmpty)
|
||||
AttireEmptySection(
|
||||
message: context
|
||||
.t
|
||||
.staff_profile_attire
|
||||
.capture
|
||||
.no_items_filter,
|
||||
)
|
||||
else
|
||||
...nonEssentialItems.map((AttireItem item) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: UiConstants.space3,
|
||||
),
|
||||
child: AttireItemCard(
|
||||
item: item,
|
||||
isUploading: false,
|
||||
uploadedPhotoUrl: state.photoUrls[item.id],
|
||||
onTap: () {
|
||||
Modular.to.toAttireCapture(
|
||||
item: item,
|
||||
initialPhotoUrl: state.photoUrls[item.id],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
const SizedBox(height: UiConstants.space20),
|
||||
],
|
||||
),
|
||||
@@ -117,3 +188,4 @@ class AttirePage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AttireEmptySection extends StatelessWidget {
|
||||
const AttireEmptySection({super.key, required this.message});
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: UiConstants.space6),
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
const Icon(UiIcons.shirt, size: 48, color: UiColors.iconInactive),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(message, style: UiTypography.body1m.textSecondary),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AttireSectionHeader extends StatelessWidget {
|
||||
const AttireSectionHeader({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.count,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final int count;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
Text(title, style: UiTypography.headline4b),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Text('($count)', style: UiTypography.body1m.textSecondary),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AttireSectionTab extends StatelessWidget {
|
||||
const AttireSectionTab({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: UiConstants.space2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? UiColors.primary : UiColors.white,
|
||||
borderRadius: UiConstants.radiusFull,
|
||||
border: Border.all(
|
||||
color: isSelected ? UiColors.primary : UiColors.border,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: isSelected
|
||||
? UiTypography.footnote2m.white
|
||||
: UiTypography.footnote2m.textSecondary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for a single attire item card.
|
||||
class AttireItemSkeleton extends StatelessWidget {
|
||||
/// Creates an [AttireItemSkeleton].
|
||||
const AttireItemSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: UiColors.border),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
color: UiColors.cardViewBackground,
|
||||
),
|
||||
child: const Row(
|
||||
children: <Widget>[
|
||||
UiShimmerBox(width: 56, height: 56),
|
||||
SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
UiShimmerLine(width: 120, height: 14),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
UiShimmerLine(width: 80, height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
UiShimmerBox(width: 60, height: 24),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'attire_item_skeleton.dart';
|
||||
|
||||
/// Full-page shimmer skeleton shown while attire items are loading.
|
||||
class AttireSkeleton extends StatelessWidget {
|
||||
/// Creates an [AttireSkeleton].
|
||||
const AttireSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiShimmer(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// Info card placeholder
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: UiColors.border),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
color: UiColors.cardViewBackground,
|
||||
),
|
||||
child: const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
UiShimmerLine(width: 160, height: 14),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
UiShimmerLine(height: 12),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
UiShimmerLine(width: 200, height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
// Section toggle chips placeholder
|
||||
const Row(
|
||||
children: <Widget>[
|
||||
UiShimmerBox(width: 80, height: 32),
|
||||
SizedBox(width: UiConstants.space3),
|
||||
UiShimmerBox(width: 100, height: 32),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
// Section header placeholder
|
||||
const UiShimmerSectionHeader(),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
// Attire item cards
|
||||
UiShimmerList(
|
||||
itemCount: 4,
|
||||
spacing: UiConstants.space3,
|
||||
itemBuilder: (int index) => const AttireItemSkeleton(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import '../widgets/emergency_contact_add_button.dart';
|
||||
import '../widgets/emergency_contact_form_item.dart';
|
||||
import '../widgets/emergency_contact_info_banner.dart';
|
||||
import '../widgets/emergency_contact_save_button.dart';
|
||||
import '../widgets/emergency_contact_skeleton/emergency_contact_skeleton.dart';
|
||||
|
||||
/// The Staff Emergency Contact screen.
|
||||
///
|
||||
@@ -43,7 +44,7 @@ class EmergencyContactScreen extends StatelessWidget {
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state.status == EmergencyContactStatus.loading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
return const EmergencyContactSkeleton();
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
|
||||
@@ -7,7 +7,9 @@ class EmergencyContactInfoBanner extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiNoticeBanner(
|
||||
title:
|
||||
icon: UiIcons.warning,
|
||||
title: 'Emergency Contact Information',
|
||||
description:
|
||||
'Please provide at least one emergency contact. This information will only be used in case of an emergency during your shifts.',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'contact_field_skeleton.dart';
|
||||
|
||||
/// Shimmer placeholder for a single emergency contact card.
|
||||
class ContactCardSkeleton extends StatelessWidget {
|
||||
/// Creates a [ContactCardSkeleton].
|
||||
const ContactCardSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space4),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// Header ("Contact 1")
|
||||
UiShimmerLine(width: 90, height: 16),
|
||||
SizedBox(height: UiConstants.space4),
|
||||
// Full Name field
|
||||
ContactFieldSkeleton(),
|
||||
SizedBox(height: UiConstants.space4),
|
||||
// Phone Number field
|
||||
ContactFieldSkeleton(),
|
||||
SizedBox(height: UiConstants.space4),
|
||||
// Relationship field
|
||||
ContactFieldSkeleton(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for a single form field (label + input).
|
||||
class ContactFieldSkeleton extends StatelessWidget {
|
||||
/// Creates a [ContactFieldSkeleton].
|
||||
const ContactFieldSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
UiShimmerLine(width: 80, height: 12),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
UiShimmerBox(width: double.infinity, height: 48),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'contact_card_skeleton.dart';
|
||||
import 'info_banner_skeleton.dart';
|
||||
|
||||
/// Full-page shimmer skeleton shown while emergency contacts are loading.
|
||||
class EmergencyContactSkeleton extends StatelessWidget {
|
||||
/// Creates an [EmergencyContactSkeleton].
|
||||
const EmergencyContactSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiShimmer(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
// Info banner
|
||||
const InfoBannerSkeleton(),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
// Contact card
|
||||
const ContactCardSkeleton(),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
// Add contact button placeholder
|
||||
UiShimmerBox(
|
||||
width: 180,
|
||||
height: 40,
|
||||
borderRadius: UiConstants.radiusFull,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Save button placeholder
|
||||
Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(top: BorderSide(color: UiColors.border)),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: UiShimmerBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for the emergency contact info banner.
|
||||
class InfoBannerSkeleton extends StatelessWidget {
|
||||
/// Creates an [InfoBannerSkeleton].
|
||||
const InfoBannerSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: UiColors.border),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
color: UiColors.cardViewBackground,
|
||||
),
|
||||
child: const Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
UiShimmerCircle(size: 24),
|
||||
SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
UiShimmerLine(height: 12),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
UiShimmerLine(width: 200, height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import 'package:krow_core/core.dart';
|
||||
import 'package:staff_profile_info/src/presentation/blocs/personal_info_bloc.dart';
|
||||
import 'package:staff_profile_info/src/presentation/blocs/personal_info_state.dart';
|
||||
import 'package:staff_profile_info/src/presentation/widgets/personal_info_page/personal_info_content.dart';
|
||||
import 'package:staff_profile_info/src/presentation/widgets/personal_info_skeleton/personal_info_skeleton.dart';
|
||||
|
||||
|
||||
/// The Personal Info page for staff onboarding.
|
||||
@@ -56,7 +57,7 @@ class PersonalInfoPage extends StatelessWidget {
|
||||
builder: (BuildContext context, PersonalInfoState state) {
|
||||
if (state.status == PersonalInfoStatus.loading ||
|
||||
state.status == PersonalInfoStatus.initial) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
return const PersonalInfoSkeleton();
|
||||
}
|
||||
|
||||
if (state.staff == null) {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for a single form field (label + input).
|
||||
class FormFieldSkeleton extends StatelessWidget {
|
||||
/// Creates a [FormFieldSkeleton].
|
||||
const FormFieldSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
UiShimmerLine(width: 80, height: 12),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
UiShimmerBox(width: double.infinity, height: 48),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'form_field_skeleton.dart';
|
||||
|
||||
/// Full-page shimmer skeleton shown while personal info is loading.
|
||||
class PersonalInfoSkeleton extends StatelessWidget {
|
||||
/// Creates a [PersonalInfoSkeleton].
|
||||
const PersonalInfoSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiShimmer(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
// Avatar placeholder
|
||||
const Center(child: UiShimmerCircle(size: 80)),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
// Form fields
|
||||
UiShimmerList(
|
||||
itemCount: 5,
|
||||
spacing: UiConstants.space5,
|
||||
itemBuilder: (int index) => const FormFieldSkeleton(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for a single FAQ accordion item.
|
||||
class FaqItemSkeleton extends StatelessWidget {
|
||||
/// Creates a [FaqItemSkeleton].
|
||||
const FaqItemSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: UiColors.border),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
color: UiColors.cardViewBackground,
|
||||
),
|
||||
child: const Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: UiShimmerLine(height: 14),
|
||||
),
|
||||
SizedBox(width: UiConstants.space3),
|
||||
UiShimmerCircle(size: 20),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'faq_item_skeleton.dart';
|
||||
|
||||
/// Full-page shimmer skeleton shown while FAQs are loading.
|
||||
class FaqsSkeleton extends StatelessWidget {
|
||||
/// Creates a [FaqsSkeleton].
|
||||
const FaqsSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiShimmer(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
UiConstants.space5,
|
||||
UiConstants.space5,
|
||||
UiConstants.space5,
|
||||
UiConstants.space24,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// Search bar placeholder
|
||||
UiShimmerBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
// Category header
|
||||
const UiShimmerSectionHeader(),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
// FAQ items
|
||||
UiShimmerList(
|
||||
itemCount: 3,
|
||||
spacing: UiConstants.space2,
|
||||
itemBuilder: (int index) => const FaqItemSkeleton(),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
// Second category
|
||||
const UiShimmerSectionHeader(),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
UiShimmerList(
|
||||
itemCount: 3,
|
||||
spacing: UiConstants.space2,
|
||||
itemBuilder: (int index) => const FaqItemSkeleton(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ 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';
|
||||
import 'faqs_skeleton/faqs_skeleton.dart';
|
||||
|
||||
/// Widget displaying FAQs with search functionality and accordion items
|
||||
class FaqsWidget extends StatefulWidget {
|
||||
@@ -76,10 +77,7 @@ class _FaqsWidgetState extends State<FaqsWidget> {
|
||||
|
||||
// FAQ List or Empty State
|
||||
if (state.isLoading)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 48),
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
const FaqsSkeleton()
|
||||
else if (state.categories.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 48),
|
||||
|
||||
@@ -17,17 +17,17 @@ class FaqsModule extends Module {
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
// Repository
|
||||
i.addSingleton<FaqsRepositoryInterface>(
|
||||
i.addLazySingleton<FaqsRepositoryInterface>(
|
||||
() => FaqsRepositoryImpl(),
|
||||
);
|
||||
|
||||
// Use Cases
|
||||
i.addSingleton(
|
||||
i.addLazySingleton(
|
||||
() => GetFaqsUseCase(
|
||||
i<FaqsRepositoryInterface>(),
|
||||
),
|
||||
);
|
||||
i.addSingleton(
|
||||
i.addLazySingleton(
|
||||
() => SearchFaqsUseCase(
|
||||
i<FaqsRepositoryInterface>(),
|
||||
),
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
|
||||
import '../../blocs/legal/privacy_policy_cubit.dart';
|
||||
import '../../widgets/skeletons/legal_document_skeleton.dart';
|
||||
|
||||
/// Page displaying the Privacy Policy document
|
||||
class PrivacyPolicyPage extends StatelessWidget {
|
||||
@@ -24,9 +25,7 @@ class PrivacyPolicyPage extends StatelessWidget {
|
||||
child: BlocBuilder<PrivacyPolicyCubit, PrivacyPolicyState>(
|
||||
builder: (BuildContext context, PrivacyPolicyState state) {
|
||||
if (state.isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
return const LegalDocumentSkeleton();
|
||||
}
|
||||
|
||||
if (state.error != null) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
|
||||
import '../../blocs/legal/terms_cubit.dart';
|
||||
import '../../widgets/skeletons/legal_document_skeleton.dart';
|
||||
|
||||
/// Page displaying the Terms of Service document
|
||||
class TermsOfServicePage extends StatelessWidget {
|
||||
@@ -24,9 +25,7 @@ class TermsOfServicePage extends StatelessWidget {
|
||||
child: BlocBuilder<TermsCubit, TermsState>(
|
||||
builder: (BuildContext context, TermsState state) {
|
||||
if (state.isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
return const LegalDocumentSkeleton();
|
||||
}
|
||||
|
||||
if (state.error != null) {
|
||||
|
||||
@@ -7,6 +7,7 @@ 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';
|
||||
import '../widgets/skeletons/privacy_security_skeleton.dart';
|
||||
|
||||
/// Page displaying privacy & security settings for staff
|
||||
class PrivacySecurityPage extends StatelessWidget {
|
||||
@@ -25,7 +26,7 @@ class PrivacySecurityPage extends StatelessWidget {
|
||||
child: BlocBuilder<PrivacySecurityBloc, PrivacySecurityState>(
|
||||
builder: (BuildContext context, PrivacySecurityState state) {
|
||||
if (state.isLoading) {
|
||||
return const UiLoadingPage();
|
||||
return const PrivacySecuritySkeleton();
|
||||
}
|
||||
|
||||
return const SingleChildScrollView(
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shared shimmer skeleton for legal document pages (Privacy Policy, Terms).
|
||||
///
|
||||
/// Simulates a long-form text document with varied line widths.
|
||||
class LegalDocumentSkeleton extends StatelessWidget {
|
||||
/// Creates a [LegalDocumentSkeleton].
|
||||
const LegalDocumentSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiShimmer(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// Title line
|
||||
const UiShimmerLine(width: 200, height: 18),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
// Body text lines with varied widths
|
||||
UiShimmerList(
|
||||
itemCount: 4,
|
||||
spacing: UiConstants.space2,
|
||||
itemBuilder: (int index) => const UiShimmerLine(),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space5),
|
||||
const UiShimmerLine(width: 180, height: 16),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
UiShimmerList(
|
||||
itemCount: 5,
|
||||
spacing: UiConstants.space2,
|
||||
itemBuilder: (int index) => UiShimmerLine(
|
||||
width: index == 4 ? 200 : double.infinity,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space5),
|
||||
const UiShimmerLine(width: 160, height: 16),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
UiShimmerList(
|
||||
itemCount: 3,
|
||||
spacing: UiConstants.space2,
|
||||
itemBuilder: (int index) => const UiShimmerLine(),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space5),
|
||||
const UiShimmerLine(width: 140, height: 16),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
UiShimmerList(
|
||||
itemCount: 4,
|
||||
spacing: UiConstants.space2,
|
||||
itemBuilder: (int index) => UiShimmerLine(
|
||||
width: index == 3 ? 160 : double.infinity,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'settings_toggle_skeleton.dart';
|
||||
|
||||
/// Full-page shimmer skeleton shown while privacy settings are loading.
|
||||
class PrivacySecuritySkeleton extends StatelessWidget {
|
||||
/// Creates a [PrivacySecuritySkeleton].
|
||||
const PrivacySecuritySkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiShimmer(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// Privacy section header
|
||||
const UiShimmerSectionHeader(),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
UiShimmerList(
|
||||
itemCount: 3,
|
||||
spacing: UiConstants.space4,
|
||||
itemBuilder: (int index) => const SettingsToggleSkeleton(),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
// Legal section header
|
||||
const UiShimmerSectionHeader(),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
// Legal links
|
||||
UiShimmerList(
|
||||
itemCount: 2,
|
||||
spacing: UiConstants.space3,
|
||||
itemBuilder: (int index) => const UiShimmerListItem(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for a single settings toggle row.
|
||||
class SettingsToggleSkeleton extends StatelessWidget {
|
||||
/// Creates a [SettingsToggleSkeleton].
|
||||
const SettingsToggleSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: UiConstants.space2),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
UiShimmerLine(width: 140, height: 14),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
UiShimmerLine(width: 200, height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: UiConstants.space3),
|
||||
UiShimmerBox(width: 48, height: 28),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -25,29 +25,29 @@ class PrivacySecurityModule extends Module {
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
// Repository
|
||||
i.addSingleton<PrivacySettingsRepositoryInterface>(
|
||||
i.addLazySingleton<PrivacySettingsRepositoryInterface>(
|
||||
() => PrivacySettingsRepositoryImpl(
|
||||
Modular.get<DataConnectService>(),
|
||||
),
|
||||
);
|
||||
|
||||
// Use Cases
|
||||
i.addSingleton(
|
||||
i.addLazySingleton(
|
||||
() => GetProfileVisibilityUseCase(
|
||||
i<PrivacySettingsRepositoryInterface>(),
|
||||
),
|
||||
);
|
||||
i.addSingleton(
|
||||
i.addLazySingleton(
|
||||
() => UpdateProfileVisibilityUseCase(
|
||||
i<PrivacySettingsRepositoryInterface>(),
|
||||
),
|
||||
);
|
||||
i.addSingleton(
|
||||
i.addLazySingleton(
|
||||
() => GetTermsUseCase(
|
||||
i<PrivacySettingsRepositoryInterface>(),
|
||||
),
|
||||
);
|
||||
i.addSingleton(
|
||||
i.addLazySingleton(
|
||||
() => GetPrivacyPolicyUseCase(
|
||||
i<PrivacySettingsRepositoryInterface>(),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import '../../../domain/usecases/apply_for_shift_usecase.dart';
|
||||
import '../../../domain/usecases/decline_shift_usecase.dart';
|
||||
import '../../../domain/usecases/get_shift_details_usecase.dart';
|
||||
@@ -12,11 +13,13 @@ class ShiftDetailsBloc extends Bloc<ShiftDetailsEvent, ShiftDetailsState>
|
||||
final GetShiftDetailsUseCase getShiftDetails;
|
||||
final ApplyForShiftUseCase applyForShift;
|
||||
final DeclineShiftUseCase declineShift;
|
||||
final GetProfileCompletionUseCase getProfileCompletion;
|
||||
|
||||
ShiftDetailsBloc({
|
||||
required this.getShiftDetails,
|
||||
required this.applyForShift,
|
||||
required this.declineShift,
|
||||
required this.getProfileCompletion,
|
||||
}) : super(ShiftDetailsInitial()) {
|
||||
on<LoadShiftDetailsEvent>(_onLoadDetails);
|
||||
on<BookShiftDetailsEvent>(_onBookShift);
|
||||
@@ -34,8 +37,9 @@ class ShiftDetailsBloc extends Bloc<ShiftDetailsEvent, ShiftDetailsState>
|
||||
final shift = await getShiftDetails(
|
||||
GetShiftDetailsArguments(shiftId: event.shiftId, roleId: event.roleId),
|
||||
);
|
||||
final isProfileComplete = await getProfileCompletion();
|
||||
if (shift != null) {
|
||||
emit(ShiftDetailsLoaded(shift));
|
||||
emit(ShiftDetailsLoaded(shift, isProfileComplete: isProfileComplete));
|
||||
} else {
|
||||
emit(const ShiftDetailsError("Shift not found"));
|
||||
}
|
||||
|
||||
@@ -14,10 +14,11 @@ class ShiftDetailsLoading extends ShiftDetailsState {}
|
||||
|
||||
class ShiftDetailsLoaded extends ShiftDetailsState {
|
||||
final Shift shift;
|
||||
const ShiftDetailsLoaded(this.shift);
|
||||
final bool isProfileComplete;
|
||||
const ShiftDetailsLoaded(this.shift, {this.isProfileComplete = false});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [shift];
|
||||
List<Object?> get props => [shift, isProfileComplete];
|
||||
}
|
||||
|
||||
class ShiftDetailsError extends ShiftDetailsState {
|
||||
|
||||
@@ -15,6 +15,7 @@ import '../widgets/shift_details/shift_date_time_section.dart';
|
||||
import '../widgets/shift_details/shift_description_section.dart';
|
||||
import '../widgets/shift_details/shift_details_bottom_bar.dart';
|
||||
import '../widgets/shift_details/shift_details_header.dart';
|
||||
import '../widgets/shift_details_page_skeleton.dart';
|
||||
import '../widgets/shift_details/shift_location_section.dart';
|
||||
import '../widgets/shift_details/shift_schedule_summary_section.dart';
|
||||
import '../widgets/shift_details/shift_stats_row.dart';
|
||||
@@ -118,13 +119,14 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is ShiftDetailsLoading) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
return const ShiftDetailsPageSkeleton();
|
||||
}
|
||||
|
||||
final Shift displayShift = widget.shift;
|
||||
final i18n = Translations.of(context).staff_shifts.shift_details;
|
||||
final isProfileComplete = state is ShiftDetailsLoaded
|
||||
? state.isProfileComplete
|
||||
: false;
|
||||
|
||||
final duration = _calculateDuration(displayShift);
|
||||
final estimatedTotal =
|
||||
@@ -142,6 +144,16 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!isProfileComplete)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
child: UiNoticeBanner(
|
||||
title: 'Complete Your Account',
|
||||
description:
|
||||
'Complete your account to book this shift and start earning',
|
||||
icon: UiIcons.sparkles,
|
||||
),
|
||||
),
|
||||
ShiftDetailsHeader(shift: displayShift),
|
||||
const Divider(height: 1, thickness: 0.5),
|
||||
ShiftStatsRow(
|
||||
@@ -194,20 +206,21 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
ShiftDetailsBottomBar(
|
||||
shift: displayShift,
|
||||
onApply: () => _bookShift(context, displayShift),
|
||||
onDecline: () => BlocProvider.of<ShiftDetailsBloc>(
|
||||
context,
|
||||
).add(DeclineShiftDetailsEvent(displayShift.id)),
|
||||
onAccept: () =>
|
||||
BlocProvider.of<ShiftDetailsBloc>(context).add(
|
||||
BookShiftDetailsEvent(
|
||||
displayShift.id,
|
||||
roleId: displayShift.roleId,
|
||||
if (isProfileComplete)
|
||||
ShiftDetailsBottomBar(
|
||||
shift: displayShift,
|
||||
onApply: () => _bookShift(context, displayShift),
|
||||
onDecline: () => BlocProvider.of<ShiftDetailsBloc>(
|
||||
context,
|
||||
).add(DeclineShiftDetailsEvent(displayShift.id)),
|
||||
onAccept: () =>
|
||||
BlocProvider.of<ShiftDetailsBloc>(context).add(
|
||||
BookShiftDetailsEvent(
|
||||
displayShift.id,
|
||||
roleId: displayShift.roleId,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -314,9 +327,9 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
backgroundColor: UiColors.bgPopup,
|
||||
shape: RoundedRectangleBorder(borderRadius: UiConstants.radiusLg),
|
||||
title: Row(
|
||||
spacing: UiConstants.space2,
|
||||
children: [
|
||||
const Icon(UiIcons.warning, color: UiColors.error),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Expanded(
|
||||
child: Text(
|
||||
context.t.staff_shifts.shift_details.eligibility_requirements,
|
||||
@@ -336,7 +349,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
UiButton.primary(
|
||||
text: "Go to Certificates",
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
Modular.to.popSafe();
|
||||
Modular.to.toCertificates();
|
||||
},
|
||||
),
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:core_localization/core_localization.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../blocs/shifts/shifts_bloc.dart';
|
||||
import '../utils/shift_tab_type.dart';
|
||||
import '../widgets/shifts_page_skeleton.dart';
|
||||
import '../widgets/tabs/my_shifts_tab.dart';
|
||||
import '../widgets/tabs/find_shifts_tab.dart';
|
||||
import '../widgets/tabs/history_shifts_tab.dart';
|
||||
@@ -196,7 +197,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
// Body Content
|
||||
Expanded(
|
||||
child: state.status == ShiftsStatus.loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
? const ShiftsPageSkeleton()
|
||||
: state.status == ShiftsStatus.error
|
||||
? Center(
|
||||
child: Padding(
|
||||
@@ -252,7 +253,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
);
|
||||
case ShiftTabType.find:
|
||||
if (availableLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
return const ShiftsPageSkeleton();
|
||||
}
|
||||
return FindShiftsTab(
|
||||
availableJobs: availableJobs,
|
||||
@@ -260,7 +261,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
);
|
||||
case ShiftTabType.history:
|
||||
if (historyLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
return const ShiftsPageSkeleton();
|
||||
}
|
||||
return HistoryShiftsTab(historyShifts: historyShifts);
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export 'shift_details_page_skeleton/index.dart';
|
||||
@@ -0,0 +1,2 @@
|
||||
export 'shift_details_page_skeleton.dart';
|
||||
export 'stat_card_skeleton.dart';
|
||||
@@ -0,0 +1,150 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'stat_card_skeleton.dart';
|
||||
|
||||
/// Shimmer loading skeleton for the shift details page.
|
||||
///
|
||||
/// Mimics the loaded layout: a header with icon + text lines, a stats row
|
||||
/// with three stat cards, and content sections with date/time and location
|
||||
/// placeholders.
|
||||
class ShiftDetailsPageSkeleton extends StatelessWidget {
|
||||
/// Creates a [ShiftDetailsPageSkeleton].
|
||||
const ShiftDetailsPageSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: const UiAppBar(centerTitle: false),
|
||||
body: UiShimmer(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header: icon box + title/subtitle lines
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
UiShimmerBox(
|
||||
width: 114,
|
||||
height: 100,
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
UiShimmerLine(width: 180, height: 20),
|
||||
SizedBox(height: UiConstants.space3),
|
||||
UiShimmerLine(width: 140, height: 14),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
UiShimmerLine(width: 200, height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const Divider(height: 1, thickness: 0.5),
|
||||
|
||||
// Stats row: three stat cards
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Row(
|
||||
children: List.generate(3, (index) {
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: index > 0 ? UiConstants.space2 : 0,
|
||||
),
|
||||
child: const StatCardSkeleton(),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
const Divider(height: 1, thickness: 0.5),
|
||||
|
||||
// Date / time section
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const UiShimmerLine(width: 100, height: 14),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const [
|
||||
UiShimmerLine(width: 80, height: 12),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
UiShimmerLine(width: 120, height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const [
|
||||
UiShimmerLine(width: 80, height: 12),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
UiShimmerLine(width: 120, height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const Divider(height: 1, thickness: 0.5),
|
||||
|
||||
// Location section
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const [
|
||||
UiShimmerLine(width: 80, height: 14),
|
||||
SizedBox(height: UiConstants.space3),
|
||||
UiShimmerLine(height: 14),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
UiShimmerLine(width: 240, height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const Divider(height: 1, thickness: 0.5),
|
||||
|
||||
// Description section
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const [
|
||||
UiShimmerLine(width: 120, height: 14),
|
||||
SizedBox(height: UiConstants.space3),
|
||||
UiShimmerLine(height: 12),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
UiShimmerLine(height: 12),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
UiShimmerLine(width: 200, height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Skeleton for a single stat card in the stats row.
|
||||
class StatCardSkeleton extends StatelessWidget {
|
||||
/// Creates a [StatCardSkeleton].
|
||||
const StatCardSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgThird,
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
),
|
||||
child: const Column(
|
||||
children: [
|
||||
UiShimmerCircle(size: 40),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
UiShimmerLine(width: 50, height: 16),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
UiShimmerLine(width: 60, height: 12),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export 'shifts_page_skeleton/index.dart';
|
||||
@@ -0,0 +1,2 @@
|
||||
export 'shift_card_skeleton.dart';
|
||||
export 'shifts_page_skeleton.dart';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user