Merge pull request #643 from Oloodi/616-implement-shimmer-loading-in-mobile-applications-fe

Implement shimmer loading states in all the pages in both of the mobile applications.
This commit is contained in:
Achintha Isuru
2026-03-10 23:57:36 -04:00
committed by GitHub
173 changed files with 4512 additions and 129 deletions

View File

@@ -0,0 +1,22 @@
# Mobile Architecture Reviewer Memory
## Project Structure
- Features: `apps/mobile/packages/features/{client,staff}/<feature>/`
- Design System: `apps/mobile/packages/design_system/`
- Shimmer primitives: `design_system/lib/src/widgets/shimmer/` (UiShimmer, UiShimmerBox, UiShimmerCircle, UiShimmerLine, presets)
- UiConstants spacing: space0=0, space1=4, space2=8, space3=12, space4=16, space5=20, space6=24, space8=32, space10=40, space12=48
## Design System Conventions
- `UiConstants.radiusLg`, `radiusMd`, `radiusSm`, `radiusFull` are `static final` (not const) - cannot use `const` at call sites
- Shimmer placeholder dimensions (width/height of boxes/lines/circles) are visual content sizes, not spacing - the design system presets use UiConstants for these too
- `Divider(height: 1, thickness: 0.5)` is a common pattern in the codebase for thin dividers
## Common Pre-Existing Issues (Do Not Flag as New)
- Report detail pages use hardcoded `top: 60` for AppBar clearance (all 6 report pages)
- Payments page and billing page have hardcoded strings (pre-existing, not part of shimmer changes)
- `shift_details_page.dart` has hardcoded strings and Navigator.of usage (pre-existing)
## Review Patterns
- Skeleton files are pure presentation widgets (StatelessWidget) - no BLoC, no business logic, no navigation
- Skeleton files only import `design_system` and `flutter/material.dart` - clean dependency
- Barrel file `index.dart` in `reports_page/` widgets dir is an internal barrel, not public API

View File

@@ -0,0 +1,55 @@
# Mobile Builder Agent Memory
## Design System - Shimmer Primitives
- Shimmer widgets are in `packages/design_system/lib/src/widgets/shimmer/`
- Available: `UiShimmer`, `UiShimmerBox`, `UiShimmerCircle`, `UiShimmerLine`, `UiShimmerListItem`, `UiShimmerStatsCard`, `UiShimmerSectionHeader`, `UiShimmerList`
- `UiShimmerList.itemBuilder` takes `(int index)` -- single parameter, not `(BuildContext, int)`
- `UiShimmerBox.borderRadius` accepts `BorderRadius?` (nullable), uses `UiConstants.radiusMd` as default
- All shimmer shapes render as solid white containers; the parent `UiShimmer` applies the animated gradient
- Exported via `design_system.dart` barrel
## Staff App Feature Locations
- Shifts: `packages/features/staff/shifts/` -- has ShiftsPage (tabbed: MyShifts/Find/History) + ShiftDetailsPage
- Home: `packages/features/staff/home/` -- WorkerHomePage with sections (TodaysShifts, TomorrowsShifts, Recommended, Benefits, QuickActions)
- Payments: `packages/features/staff/payments/` -- PaymentsPage with gradient header + stats + payment history
- Home cubit: `HomeStatus` enum (initial, loading, loaded, error)
- Shifts bloc: `ShiftsStatus` enum + sub-loading flags (`availableLoading`, `historyLoading`)
- Payments bloc: uses sealed state classes (`PaymentsLoading`, `PaymentsLoaded`, `PaymentsError`)
## UiConstants Spacing Tokens
- Use `UiConstants.space1` through `UiConstants.space24` for spacing
- Radius: `UiConstants.radiusSm`, `radiusMd`, `radiusLg`, `radiusFull`, `radiusBase`, `radiusMdValue` (double)
- `UiConstants.radiusFull` is a `BorderRadius`, `UiConstants.radiusMdValue` is a `double`
## Barrel Files (Staff Features)
- Shifts: `lib/staff_shifts.dart` exports modules only
- Payments: `lib/staff_payements.dart` (note: typo in filename) exports module only
- Home: `lib/staff_home.dart` exports module only
- These barrel files only export modules, not individual widgets -- skeleton widgets don't need to be added
## Client App Feature Locations
- Coverage: `packages/features/client/client_coverage/`
- Home: `packages/features/client/home/` (no loading spinner -- renders default data during load)
- Billing: `packages/features/client/billing/` (billing_page, pending_invoices_page, invoice_ready_page)
- Reports: `packages/features/client/reports/` (reports_page with metrics_grid, plus 6 sub-report pages)
- Reports barrel: `widgets/reports_page/index.dart`
- Hubs: `packages/features/client/hubs/` (client_hubs_page + hub_details_page + edit_hub_page)
## Staff Profile Sections (shimmer done)
- Compliance: certificates, documents, tax_forms -- all have shimmer skeletons
- Finances: staff_bank_account, time_card -- all have shimmer skeletons
- Onboarding: attire, profile_info (personal_info_page only) -- have shimmer skeletons
- Support: faqs, privacy_security (including legal sub-pages) -- have shimmer skeletons
- Pages that intentionally keep CircularProgressIndicator (action/submit spinners):
- form_i9_page, form_w4_page (submit button spinners)
- experience_page (save button spinner)
- preferred_locations_page (save button + overlay spinner)
- certificate_upload_page, document_upload_page, attire_capture_page (form/upload pages, no initial load)
- language_selection_page (no loading state, static list)
- LegalDocumentSkeleton is shared between PrivacyPolicyPage and TermsOfServicePage
## Key Patterns Observed
- BenefitsOverviewPage also has CircularProgressIndicator (not shimmer-ified yet)
- ShiftDetailsPage has a dialog-level spinner in the "applying" dialog -- this is intentional, not a page loading state
- Hub details/edit pages use CircularProgressIndicator as action overlays (save/delete) -- keep as-is, not initial load
- Client home page has no loading spinner; it renders with default empty dashboard data

View File

@@ -223,7 +223,9 @@ A PR is approved ONLY when ALL of these are true:
- Zero CRITICAL violations
- Zero HIGH violations
- MODERATE violations have a documented plan or justification
- All automated checks pass (tests, linting)
- All automated checks pass
- defined tests
- defined lints including the dart analyzer with no warnings or errors
- Test coverage ≥ 70% for business logic
- Design system fully compliant
- Architecture boundaries fully respected

3
.gitignore vendored
View File

@@ -187,6 +187,9 @@ krow-workforce-export-latest/
apps/mobile/packages/data_connect/lib/src/dataconnect_generated/
apps/web/src/dataconnect-generated/
# Legacy mobile applications
apps/mobile/legacy/*
AGENTS.md
TASKS.md

View File

@@ -325,6 +325,8 @@
"client_create_order": {
"title": "Create Order",
"section_title": "ORDER TYPE",
"no_vendors_title": "No Vendors Available",
"no_vendors_description": "There are no staffing vendors associated with your account.",
"types": {
"rapid": "RAPID",
"rapid_desc": "URGENT same-day Coverage",

View File

@@ -325,6 +325,8 @@
"client_create_order": {
"title": "Crear Orden",
"section_title": "TIPO DE ORDEN",
"no_vendors_title": "No Hay Proveedores Disponibles",
"no_vendors_description": "No hay proveedores de personal asociados con su cuenta.",
"types": {
"rapid": "R\u00c1PIDO",
"rapid_desc": "Cobertura URGENTE mismo d\u00eda",

View File

@@ -14,3 +14,6 @@ export 'src/widgets/ui_loading_page.dart';
export 'src/widgets/ui_snackbar.dart';
export 'src/widgets/ui_notice_banner.dart';
export 'src/widgets/ui_empty_state.dart';
export 'src/widgets/shimmer/ui_shimmer.dart';
export 'src/widgets/shimmer/ui_shimmer_shapes.dart';
export 'src/widgets/shimmer/ui_shimmer_presets.dart';

View File

@@ -0,0 +1,27 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
/// Core shimmer wrapper that applies an animated gradient effect to its child.
///
/// Wraps the `shimmer` package's [Shimmer.fromColors] using design system
/// color tokens. Place shimmer shape primitives as children.
class UiShimmer extends StatelessWidget {
/// Creates a shimmer effect wrapper around [child].
const UiShimmer({
super.key,
required this.child,
});
/// The widget tree to apply the shimmer gradient over.
final Widget child;
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: UiColors.muted,
highlightColor: UiColors.background,
child: child,
);
}
}

View File

@@ -0,0 +1,122 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// List-row shimmer skeleton with a leading circle, two text lines, and a
/// trailing box.
///
/// Mimics a typical list item layout during loading. Wrap with [UiShimmer]
/// to activate the animated gradient.
class UiShimmerListItem extends StatelessWidget {
/// Creates a list-row shimmer skeleton.
const UiShimmerListItem({super.key});
@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.symmetric(
vertical: UiConstants.space2,
),
child: Row(
spacing: UiConstants.space3,
children: <Widget>[
UiShimmerCircle(size: UiConstants.space10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: UiConstants.space2,
children: <Widget>[
UiShimmerLine(width: 160),
UiShimmerLine(width: 100, height: 12),
],
),
),
UiShimmerBox(width: 48, height: 24),
],
),
);
}
}
/// Stats-card shimmer skeleton with an icon placeholder, a short label line,
/// and a taller value line.
///
/// Wrapped in a bordered container matching the design system card pattern.
/// Wrap with [UiShimmer] to activate the animated gradient.
class UiShimmerStatsCard extends StatelessWidget {
/// Creates a stats-card shimmer skeleton.
const UiShimmerStatsCard({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>[
UiShimmerCircle(size: UiConstants.space8),
SizedBox(height: UiConstants.space3),
UiShimmerLine(width: 80, height: 12),
SizedBox(height: UiConstants.space2),
UiShimmerLine(width: 120, height: 20),
],
),
);
}
}
/// Section-header shimmer skeleton rendering a single wide line placeholder.
///
/// Wrap with [UiShimmer] to activate the animated gradient.
class UiShimmerSectionHeader extends StatelessWidget {
/// Creates a section-header shimmer skeleton.
const UiShimmerSectionHeader({super.key});
@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: UiConstants.space2),
child: UiShimmerLine(width: 200, height: 18),
);
}
}
/// Repeats a shimmer widget [itemCount] times in a [Column] with spacing.
///
/// Use [itemBuilder] to produce each item. Wrap the entire list with
/// [UiShimmer] to share a single animated gradient across all items.
class UiShimmerList extends StatelessWidget {
/// Creates a shimmer list with [itemCount] items built by [itemBuilder].
const UiShimmerList({
super.key,
required this.itemBuilder,
this.itemCount = 3,
this.spacing,
});
/// Builder that produces each shimmer placeholder item by index.
final Widget Function(int index) itemBuilder;
/// Number of shimmer items to render. Defaults to 3.
final int itemCount;
/// Vertical spacing between items. Defaults to [UiConstants.space3].
final double? spacing;
@override
Widget build(BuildContext context) {
final double gap = spacing ?? UiConstants.space3;
return Column(
children: List<Widget>.generate(itemCount, (int index) {
return Padding(
padding: EdgeInsets.only(bottom: index < itemCount - 1 ? gap : 0),
child: itemBuilder(index),
);
}),
);
}
}

View File

@@ -0,0 +1,95 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Rectangular shimmer placeholder with configurable dimensions and corner radius.
///
/// Renders as a solid white container; the parent [UiShimmer] applies the
/// animated gradient.
class UiShimmerBox extends StatelessWidget {
/// Creates a rectangular shimmer placeholder.
const UiShimmerBox({
super.key,
required this.width,
required this.height,
this.borderRadius,
});
/// Width of the placeholder rectangle.
final double width;
/// Height of the placeholder rectangle.
final double height;
/// Corner radius. Defaults to [UiConstants.radiusMd] when null.
final BorderRadius? borderRadius;
@override
Widget build(BuildContext context) {
return Container(
width: width,
height: height,
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: borderRadius ?? UiConstants.radiusMd,
),
);
}
}
/// Circular shimmer placeholder with a configurable diameter.
///
/// Renders as a solid white circle; the parent [UiShimmer] applies the
/// animated gradient.
class UiShimmerCircle extends StatelessWidget {
/// Creates a circular shimmer placeholder with the given [size] as diameter.
const UiShimmerCircle({
super.key,
required this.size,
});
/// Diameter of the circle.
final double size;
@override
Widget build(BuildContext context) {
return Container(
width: size,
height: size,
decoration: const BoxDecoration(
color: UiColors.white,
shape: BoxShape.circle,
),
);
}
}
/// Text-line shimmer placeholder with configurable width and height.
///
/// Useful for simulating a single line of text. Renders as a solid white
/// rounded rectangle; the parent [UiShimmer] applies the animated gradient.
class UiShimmerLine extends StatelessWidget {
/// Creates a text-line shimmer placeholder.
const UiShimmerLine({
super.key,
this.width = double.infinity,
this.height = 14,
});
/// Width of the line. Defaults to [double.infinity].
final double width;
/// Height of the line. Defaults to 14 logical pixels.
final double height;
@override
Widget build(BuildContext context) {
return Container(
width: width,
height: height,
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusSm,
),
);
}
}

View File

@@ -1,8 +1,6 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import '../ui_constants.dart';
/// A customizable notice banner widget for displaying informational messages.
///
/// [UiNoticeBanner] displays a message with an optional icon and supports

View File

@@ -15,6 +15,7 @@ dependencies:
google_fonts: ^7.0.2
lucide_icons: ^0.257.0
font_awesome_flutter: ^10.7.0
shimmer: ^3.0.0
dev_dependencies:
flutter_test:

View File

@@ -8,6 +8,7 @@ import 'package:krow_core/core.dart';
import '../blocs/billing_bloc.dart';
import '../blocs/billing_event.dart';
import '../blocs/billing_state.dart';
import '../widgets/billing_page_skeleton.dart';
import '../widgets/invoice_history_section.dart';
import '../widgets/pending_invoices_section.dart';
import '../widgets/spending_breakdown_card.dart';
@@ -179,10 +180,7 @@ class _BillingViewState extends State<BillingView> {
Widget _buildContent(BuildContext context, BillingState state) {
if (state.status == BillingStatus.loading) {
return const Padding(
padding: EdgeInsets.all(UiConstants.space10),
child: Center(child: CircularProgressIndicator()),
);
return const BillingPageSkeleton();
}
if (state.status == BillingStatus.failure) {

View File

@@ -7,6 +7,7 @@ import '../blocs/billing_bloc.dart';
import '../blocs/billing_event.dart';
import '../blocs/billing_state.dart';
import '../models/billing_invoice_model.dart';
import '../widgets/invoices_list_skeleton.dart';
class InvoiceReadyPage extends StatelessWidget {
const InvoiceReadyPage({super.key});
@@ -30,7 +31,7 @@ class InvoiceReadyView extends StatelessWidget {
body: BlocBuilder<BillingBloc, BillingState>(
builder: (BuildContext context, BillingState state) {
if (state.status == BillingStatus.loading) {
return const Center(child: CircularProgressIndicator());
return const InvoicesListSkeleton();
}
if (state.invoiceHistory.isEmpty) {

View File

@@ -7,6 +7,7 @@ import 'package:krow_core/core.dart';
import '../blocs/billing_bloc.dart';
import '../blocs/billing_state.dart';
import '../widgets/invoices_list_skeleton.dart';
import '../widgets/pending_invoices_section.dart';
class PendingInvoicesPage extends StatelessWidget {
@@ -31,7 +32,7 @@ class PendingInvoicesPage extends StatelessWidget {
Widget _buildBody(BuildContext context, BillingState state) {
if (state.status == BillingStatus.loading) {
return const Center(child: CircularProgressIndicator());
return const InvoicesListSkeleton();
}
if (state.pendingInvoices.isEmpty) {

View File

@@ -0,0 +1 @@
export 'billing_page_skeleton/index.dart';

View File

@@ -0,0 +1,67 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'breakdown_row_skeleton.dart';
import 'invoice_card_skeleton.dart';
/// Shimmer loading skeleton for the billing page content area.
///
/// Mimics the loaded layout with a pending invoices section,
/// a spending breakdown card, and an invoice history list.
class BillingPageSkeleton extends StatelessWidget {
/// Creates a [BillingPageSkeleton].
const BillingPageSkeleton({super.key});
@override
Widget build(BuildContext context) {
return UiShimmer(
child: Padding(
padding: const EdgeInsets.all(UiConstants.space5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Pending invoices section header
const UiShimmerSectionHeader(),
const SizedBox(height: UiConstants.space3),
// Pending invoice cards
const InvoiceCardSkeleton(),
const SizedBox(height: UiConstants.space4),
const InvoiceCardSkeleton(),
const SizedBox(height: UiConstants.space6),
// Spending breakdown card
Container(
padding: const EdgeInsets.all(UiConstants.space5),
decoration: BoxDecoration(
border: Border.all(color: UiColors.border),
borderRadius: UiConstants.radiusLg,
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
UiShimmerLine(width: 160, height: 16),
SizedBox(height: UiConstants.space4),
// Breakdown rows
BreakdownRowSkeleton(),
SizedBox(height: UiConstants.space3),
BreakdownRowSkeleton(),
SizedBox(height: UiConstants.space3),
BreakdownRowSkeleton(),
],
),
),
const SizedBox(height: UiConstants.space6),
// Invoice history section header
const UiShimmerSectionHeader(),
const SizedBox(height: UiConstants.space3),
const UiShimmerListItem(),
const UiShimmerListItem(),
const UiShimmerListItem(),
],
),
),
);
}
}

View File

@@ -0,0 +1,19 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer placeholder for a spending breakdown row.
class BreakdownRowSkeleton extends StatelessWidget {
/// Creates a [BreakdownRowSkeleton].
const BreakdownRowSkeleton({super.key});
@override
Widget build(BuildContext context) {
return const Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
UiShimmerLine(width: 100, height: 14),
UiShimmerLine(width: 60, height: 14),
],
);
}
}

View File

@@ -0,0 +1,3 @@
export 'billing_page_skeleton.dart';
export 'breakdown_row_skeleton.dart';
export 'invoice_card_skeleton.dart';

View File

@@ -0,0 +1,58 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer placeholder for a single pending invoice card.
class InvoiceCardSkeleton extends StatelessWidget {
/// Creates an [InvoiceCardSkeleton].
const InvoiceCardSkeleton({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,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
UiShimmerBox(
width: 72,
height: 24,
borderRadius: UiConstants.radiusFull,
),
const UiShimmerLine(width: 80, height: 12),
],
),
const SizedBox(height: UiConstants.space4),
const UiShimmerLine(width: 200, height: 16),
const SizedBox(height: UiConstants.space2),
const UiShimmerLine(width: 160, height: 12),
const SizedBox(height: UiConstants.space4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
UiShimmerLine(width: 80, height: 10),
SizedBox(height: UiConstants.space1),
UiShimmerLine(width: 100, height: 18),
],
),
UiShimmerBox(
width: 100,
height: 36,
borderRadius: UiConstants.radiusMd,
),
],
),
],
),
);
}
}

View File

@@ -0,0 +1,75 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer loading skeleton for invoice list pages.
///
/// Used by both [PendingInvoicesPage] and [InvoiceReadyPage] to show
/// placeholder cards while data loads.
class InvoicesListSkeleton extends StatelessWidget {
/// Creates an [InvoicesListSkeleton].
const InvoicesListSkeleton({super.key});
@override
Widget build(BuildContext context) {
return UiShimmer(
child: Padding(
padding: const EdgeInsets.all(UiConstants.space5),
child: Column(
children: List.generate(4, (int index) {
return Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space4),
child: Container(
padding: const EdgeInsets.all(UiConstants.space5),
decoration: BoxDecoration(
border: Border.all(color: UiColors.border),
borderRadius: UiConstants.radiusLg,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
UiShimmerBox(
width: 64,
height: 22,
borderRadius: UiConstants.radiusFull,
),
const UiShimmerLine(width: 80, height: 12),
],
),
const SizedBox(height: UiConstants.space4),
const UiShimmerLine(width: 180, height: 16),
const SizedBox(height: UiConstants.space2),
const UiShimmerLine(width: 140, height: 12),
const SizedBox(height: UiConstants.space4),
const Divider(color: UiColors.border),
const SizedBox(height: UiConstants.space3),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
UiShimmerLine(width: 80, height: 10),
SizedBox(height: UiConstants.space1),
UiShimmerLine(width: 100, height: 20),
],
),
UiShimmerBox(
width: 100,
height: 36,
borderRadius: UiConstants.radiusMd,
),
],
),
],
),
),
);
}),
),
),
);
}
}

View File

@@ -9,6 +9,7 @@ import '../blocs/coverage_bloc.dart';
import '../blocs/coverage_event.dart';
import '../blocs/coverage_state.dart';
import '../widgets/coverage_calendar_selector.dart';
import '../widgets/coverage_page_skeleton.dart';
import '../widgets/coverage_quick_stats.dart';
import '../widgets/coverage_shift_list.dart';
import '../widgets/coverage_stats_header.dart';
@@ -180,9 +181,7 @@ class _CoveragePageState extends State<CoveragePage> {
}) {
if (state.shifts.isEmpty) {
if (state.status == CoverageStatus.loading) {
return const Center(
child: CircularProgressIndicator(),
);
return const CoveragePageSkeleton();
}
if (state.status == CoverageStatus.failure) {

View File

@@ -0,0 +1 @@
export 'coverage_page_skeleton/index.dart';

View File

@@ -0,0 +1,47 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'shift_card_skeleton.dart';
/// Shimmer loading skeleton that mimics the coverage page loaded layout.
///
/// Shows placeholder shapes for the quick stats row, shift section header,
/// and a list of shift cards with worker rows.
class CoveragePageSkeleton extends StatelessWidget {
/// Creates a [CoveragePageSkeleton].
const CoveragePageSkeleton({super.key});
@override
Widget build(BuildContext context) {
return UiShimmer(
child: Padding(
padding: const EdgeInsets.all(UiConstants.space5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Quick stats row (2 stat cards)
const Row(
children: [
Expanded(child: UiShimmerStatsCard()),
SizedBox(width: UiConstants.space2),
Expanded(child: UiShimmerStatsCard()),
],
),
const SizedBox(height: UiConstants.space6),
// Shifts section header
const UiShimmerLine(width: 140, height: 18),
const SizedBox(height: UiConstants.space6),
// Shift cards with worker rows
const ShiftCardSkeleton(),
const SizedBox(height: UiConstants.space3),
const ShiftCardSkeleton(),
const SizedBox(height: UiConstants.space3),
const ShiftCardSkeleton(),
],
),
),
);
}
}

View File

@@ -0,0 +1,2 @@
export 'coverage_page_skeleton.dart';
export 'shift_card_skeleton.dart';

View File

@@ -0,0 +1,60 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer placeholder for a single shift card with header and worker rows.
class ShiftCardSkeleton extends StatelessWidget {
/// Creates a [ShiftCardSkeleton].
const ShiftCardSkeleton({super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: UiColors.border),
borderRadius: UiConstants.radiusLg,
),
clipBehavior: Clip.antiAlias,
child: Column(
children: [
// Shift header
Padding(
padding: const EdgeInsets.all(UiConstants.space4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const UiShimmerLine(width: 180, height: 16),
const SizedBox(height: UiConstants.space2),
const UiShimmerLine(width: 120, height: 12),
const SizedBox(height: UiConstants.space2),
Row(
children: [
const UiShimmerLine(width: 80, height: 12),
const Spacer(),
UiShimmerBox(
width: 60,
height: 24,
borderRadius: UiConstants.radiusFull,
),
],
),
],
),
),
// Worker rows
Padding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space3,
).copyWith(bottom: UiConstants.space3),
child: const Column(
children: [
UiShimmerListItem(),
UiShimmerListItem(),
],
),
),
],
),
);
}
}

View File

@@ -8,10 +8,12 @@ import '../blocs/client_home_state.dart';
import 'client_home_edit_mode_body.dart';
import 'client_home_error_state.dart';
import 'client_home_normal_mode_body.dart';
import 'client_home_page_skeleton.dart';
/// Main body widget for the client home page.
///
/// Manages the state transitions between error, edit mode, and normal mode views.
/// Manages the state transitions between loading, error, edit mode,
/// and normal mode views.
class ClientHomeBody extends StatelessWidget {
/// Creates a [ClientHomeBody].
const ClientHomeBody({super.key});
@@ -31,6 +33,10 @@ class ClientHomeBody extends StatelessWidget {
}
},
builder: (BuildContext context, ClientHomeState state) {
if (state.status == ClientHomeStatus.initial ||
state.status == ClientHomeStatus.loading) {
return const ClientHomePageSkeleton();
}
if (state.status == ClientHomeStatus.error) {
return ClientHomeErrorState(state: state);
}

View File

@@ -22,8 +22,15 @@ class ClientHomeEditBanner extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<ClientHomeBloc, ClientHomeState>(
buildWhen: (ClientHomeState prev, ClientHomeState curr) => prev.isEditMode != curr.isEditMode,
buildWhen: (ClientHomeState prev, ClientHomeState curr) =>
prev.isEditMode != curr.isEditMode ||
prev.status != curr.status,
builder: (BuildContext context, ClientHomeState state) {
if (state.status == ClientHomeStatus.initial ||
state.status == ClientHomeStatus.loading) {
return const SizedBox.shrink();
}
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
height: state.isEditMode ? 80 : 0,

View File

@@ -7,6 +7,7 @@ import '../blocs/client_home_bloc.dart';
import '../blocs/client_home_event.dart';
import '../blocs/client_home_state.dart';
import 'header_icon_button.dart';
import 'client_home_header_skeleton.dart';
/// The header section of the client home page.
///
@@ -26,6 +27,11 @@ class ClientHomeHeader extends StatelessWidget {
Widget build(BuildContext context) {
return BlocBuilder<ClientHomeBloc, ClientHomeState>(
builder: (BuildContext context, ClientHomeState state) {
if (state.status == ClientHomeStatus.initial ||
state.status == ClientHomeStatus.loading) {
return const ClientHomeHeaderSkeleton();
}
final String businessName = state.businessName;
final String? photoUrl = state.photoUrl;
final String avatarLetter = businessName.trim().isNotEmpty

View File

@@ -0,0 +1,50 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer placeholder for the client home header during loading.
///
/// Mimics the avatar, welcome text, business name, and action buttons.
class ClientHomeHeaderSkeleton extends StatelessWidget {
/// Creates a [ClientHomeHeaderSkeleton].
const ClientHomeHeaderSkeleton({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(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: <Widget>[
const UiShimmerCircle(size: UiConstants.space10),
const SizedBox(width: UiConstants.space3),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const <Widget>[
UiShimmerLine(width: 80, height: 12),
SizedBox(height: UiConstants.space1),
UiShimmerLine(width: 120, height: 16),
],
),
],
),
Row(
spacing: UiConstants.space2,
children: const <Widget>[
UiShimmerBox(width: 36, height: 36),
UiShimmerBox(width: 36, height: 36),
],
),
],
),
),
);
}
}

View File

@@ -0,0 +1,10 @@
export 'client_home_page_skeleton/action_card_skeleton.dart';
export 'client_home_page_skeleton/actions_section_skeleton.dart';
export 'client_home_page_skeleton/client_home_page_skeleton.dart';
export 'client_home_page_skeleton/coverage_section_skeleton.dart';
export 'client_home_page_skeleton/live_activity_section_skeleton.dart';
export 'client_home_page_skeleton/metric_card_skeleton.dart';
export 'client_home_page_skeleton/reorder_card_skeleton.dart';
export 'client_home_page_skeleton/reorder_section_skeleton.dart';
export 'client_home_page_skeleton/spending_card_skeleton.dart';
export 'client_home_page_skeleton/spending_section_skeleton.dart';

View File

@@ -0,0 +1,28 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Skeleton for a single action card with icon, title, and subtitle.
class ActionCardSkeleton extends StatelessWidget {
/// Creates an [ActionCardSkeleton].
const ActionCardSkeleton({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
border: Border.all(color: UiColors.border, width: 0.5),
borderRadius: UiConstants.radiusLg,
),
child: const Column(
children: <Widget>[
UiShimmerBox(width: 36, height: 36),
SizedBox(height: UiConstants.space2),
UiShimmerLine(width: 60, height: 14),
SizedBox(height: UiConstants.space1),
UiShimmerLine(width: 100, height: 10),
],
),
);
}
}

View File

@@ -0,0 +1,28 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'action_card_skeleton.dart';
/// Skeleton for the two side-by-side action cards.
class ActionsSectionSkeleton extends StatelessWidget {
/// Creates an [ActionsSectionSkeleton].
const ActionsSectionSkeleton({super.key});
@override
Widget build(BuildContext context) {
return const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UiShimmerSectionHeader(),
SizedBox(height: UiConstants.space2),
Row(
children: <Widget>[
Expanded(child: ActionCardSkeleton()),
SizedBox(width: UiConstants.space4),
Expanded(child: ActionCardSkeleton()),
],
),
],
);
}
}

View File

@@ -0,0 +1,69 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'actions_section_skeleton.dart';
import 'coverage_section_skeleton.dart';
import 'live_activity_section_skeleton.dart';
import 'reorder_section_skeleton.dart';
import 'spending_section_skeleton.dart';
/// Shimmer loading skeleton for the client home page.
///
/// Mimics the loaded dashboard layout with action cards, reorder cards,
/// coverage metrics, spending card, and live activity sections.
class ClientHomePageSkeleton extends StatelessWidget {
/// Creates a [ClientHomePageSkeleton].
const ClientHomePageSkeleton({super.key});
@override
Widget build(BuildContext context) {
return UiShimmer(
child: ListView(
children: const <Widget>[
// Actions section
Padding(
padding: EdgeInsets.symmetric(horizontal: UiConstants.space4),
child: ActionsSectionSkeleton(),
),
SizedBox(height: UiConstants.space8),
Divider(color: UiColors.border, height: 0.1),
SizedBox(height: UiConstants.space8),
// Reorder section
Padding(
padding: EdgeInsets.symmetric(horizontal: UiConstants.space4),
child: ReorderSectionSkeleton(),
),
SizedBox(height: UiConstants.space8),
Divider(color: UiColors.border, height: 0.1),
SizedBox(height: UiConstants.space8),
// Coverage section
Padding(
padding: EdgeInsets.symmetric(horizontal: UiConstants.space4),
child: CoverageSectionSkeleton(),
),
SizedBox(height: UiConstants.space8),
Divider(color: UiColors.border, height: 0.1),
SizedBox(height: UiConstants.space8),
// Spending section
Padding(
padding: EdgeInsets.symmetric(horizontal: UiConstants.space4),
child: SpendingSectionSkeleton(),
),
SizedBox(height: UiConstants.space8),
Divider(color: UiColors.border, height: 0.1),
SizedBox(height: UiConstants.space8),
// Live activity section
Padding(
padding: EdgeInsets.symmetric(horizontal: UiConstants.space4),
child: LiveActivitySectionSkeleton(),
),
SizedBox(height: UiConstants.space8),
],
),
);
}
}

View File

@@ -0,0 +1,30 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'metric_card_skeleton.dart';
/// Skeleton for the coverage metric cards row.
class CoverageSectionSkeleton extends StatelessWidget {
/// Creates a [CoverageSectionSkeleton].
const CoverageSectionSkeleton({super.key});
@override
Widget build(BuildContext context) {
return const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UiShimmerSectionHeader(),
SizedBox(height: UiConstants.space2),
Row(
children: <Widget>[
Expanded(child: MetricCardSkeleton()),
SizedBox(width: UiConstants.space2),
Expanded(child: MetricCardSkeleton()),
SizedBox(width: UiConstants.space2),
Expanded(child: MetricCardSkeleton()),
],
),
],
);
}
}

View File

@@ -0,0 +1,23 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Skeleton for the live activity section.
class LiveActivitySectionSkeleton extends StatelessWidget {
/// Creates a [LiveActivitySectionSkeleton].
const LiveActivitySectionSkeleton({super.key});
@override
Widget build(BuildContext context) {
return const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UiShimmerSectionHeader(),
SizedBox(height: UiConstants.space2),
UiShimmerStatsCard(),
SizedBox(height: UiConstants.space3),
UiShimmerListItem(),
UiShimmerListItem(),
],
);
}
}

View File

@@ -0,0 +1,33 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Skeleton for a single coverage metric card.
class MetricCardSkeleton extends StatelessWidget {
/// Creates a [MetricCardSkeleton].
const MetricCardSkeleton({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(UiConstants.space2),
decoration: BoxDecoration(
border: Border.all(color: UiColors.border, width: 0.5),
borderRadius: UiConstants.radiusLg,
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
UiShimmerCircle(size: 14),
SizedBox(width: UiConstants.space1),
UiShimmerLine(width: 40, height: 10),
],
),
SizedBox(height: UiConstants.space2),
UiShimmerLine(width: 32, height: 20),
],
),
);
}
}

View File

@@ -0,0 +1,62 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Skeleton for a single reorder card.
class ReorderCardSkeleton extends StatelessWidget {
/// Creates a [ReorderCardSkeleton].
const ReorderCardSkeleton({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(UiConstants.space3),
decoration: BoxDecoration(
border: Border.all(color: UiColors.border, width: 0.6),
borderRadius: UiConstants.radiusLg,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Row(
children: <Widget>[
UiShimmerBox(width: 36, height: 36),
SizedBox(width: UiConstants.space2),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UiShimmerLine(width: 100, height: 14),
SizedBox(height: UiConstants.space1),
UiShimmerLine(width: 80, height: 10),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
UiShimmerLine(width: 40, height: 14),
SizedBox(height: UiConstants.space1),
UiShimmerLine(width: 60, height: 10),
],
),
],
),
const SizedBox(height: UiConstants.space3),
const Row(
children: <Widget>[
UiShimmerBox(width: 60, height: 22),
SizedBox(width: UiConstants.space2),
UiShimmerBox(width: 36, height: 22),
],
),
const Spacer(),
UiShimmerBox(
width: double.infinity,
height: 32,
borderRadius: UiConstants.radiusLg,
),
],
),
);
}
}

View File

@@ -0,0 +1,31 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'reorder_card_skeleton.dart';
/// Skeleton for the horizontal reorder cards list.
class ReorderSectionSkeleton extends StatelessWidget {
/// Creates a [ReorderSectionSkeleton].
const ReorderSectionSkeleton({super.key});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const <Widget>[
UiShimmerSectionHeader(),
SizedBox(height: UiConstants.space2),
SizedBox(
height: 164,
child: Row(
children: <Widget>[
Expanded(child: ReorderCardSkeleton()),
SizedBox(width: UiConstants.space3),
Expanded(child: ReorderCardSkeleton()),
],
),
),
],
);
}
}

View File

@@ -0,0 +1,47 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Skeleton mimicking the spending card layout.
class SpendingCardSkeleton extends StatelessWidget {
/// Creates a [SpendingCardSkeleton].
const SpendingCardSkeleton({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: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UiShimmerLine(width: 60, height: 10),
SizedBox(height: UiConstants.space1),
UiShimmerLine(width: 80, height: 22),
SizedBox(height: UiConstants.space1),
UiShimmerLine(width: 50, height: 10),
],
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
UiShimmerLine(width: 60, height: 10),
SizedBox(height: UiConstants.space1),
UiShimmerLine(width: 70, height: 18),
SizedBox(height: UiConstants.space1),
UiShimmerLine(width: 50, height: 10),
],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,22 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'spending_card_skeleton.dart';
/// Skeleton for the spending gradient card.
class SpendingSectionSkeleton extends StatelessWidget {
/// Creates a [SpendingSectionSkeleton].
const SpendingSectionSkeleton({super.key});
@override
Widget build(BuildContext context) {
return const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UiShimmerSectionHeader(),
SizedBox(height: UiConstants.space2),
SpendingCardSkeleton(),
],
);
}
}

View File

@@ -12,6 +12,7 @@ import '../blocs/client_hubs_state.dart';
import '../widgets/hub_card.dart';
import '../widgets/hub_empty_state.dart';
import '../widgets/hub_info_card.dart';
import '../widgets/hubs_page_skeleton.dart';
/// The main page for the client hubs feature.
///
@@ -94,7 +95,7 @@ class ClientHubsPage extends StatelessWidget {
),
if (state.status == ClientHubsStatus.loading)
const Center(child: CircularProgressIndicator())
const HubsPageSkeleton()
else if (state.hubs.isEmpty)
HubEmptyState(
onAddPressed: () async {

View File

@@ -0,0 +1,56 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer loading skeleton for the hubs list page.
///
/// Shows placeholder hub cards matching the [HubCard] layout with a
/// leading icon box, title line, and address line.
class HubsPageSkeleton extends StatelessWidget {
/// Creates a [HubsPageSkeleton].
const HubsPageSkeleton({super.key});
@override
Widget build(BuildContext context) {
return UiShimmer(
child: Column(
children: List.generate(5, (int index) {
return Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space3),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: UiColors.border),
borderRadius: UiConstants.radiusLg,
),
padding: const EdgeInsets.all(UiConstants.space4),
child: Row(
children: [
// Leading icon placeholder
UiShimmerBox(
width: 52,
height: 52,
borderRadius: UiConstants.radiusLg,
),
const SizedBox(width: UiConstants.space4),
// Title and address lines
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
UiShimmerLine(width: 160, height: 16),
SizedBox(height: UiConstants.space2),
UiShimmerLine(width: 200, height: 12),
],
),
),
const SizedBox(width: UiConstants.space3),
// Chevron placeholder
const UiShimmerBox(width: 16, height: 16),
],
),
),
);
}),
),
);
}
}

View File

@@ -79,7 +79,7 @@ class ClientCreateOrderModule extends Module {
@override
void routes(RouteManager r) {
r.child(
'/',
ClientPaths.childRoute(ClientPaths.createOrder, ClientPaths.createOrder),
child: (BuildContext context) => const ClientCreateOrderPage(),
);
r.child(

View File

@@ -149,7 +149,11 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
? event.vendors.first
: null;
emit(
state.copyWith(vendors: event.vendors, selectedVendor: selectedVendor),
state.copyWith(
vendors: event.vendors,
selectedVendor: selectedVendor,
isDataLoaded: true,
),
);
if (selectedVendor != null) {
await _loadRolesForVendor(selectedVendor.id, emit);

View File

@@ -21,6 +21,7 @@ class OneTimeOrderState extends Equatable {
this.managers = const <OneTimeOrderManagerOption>[],
this.selectedManager,
this.isRapidDraft = false,
this.isDataLoaded = false,
});
factory OneTimeOrderState.initial() {
@@ -52,6 +53,9 @@ class OneTimeOrderState extends Equatable {
final OneTimeOrderManagerOption? selectedManager;
final bool isRapidDraft;
/// Whether initial data (vendors, hubs) has been fetched from the backend.
final bool isDataLoaded;
OneTimeOrderState copyWith({
DateTime? date,
String? location,
@@ -67,6 +71,7 @@ class OneTimeOrderState extends Equatable {
List<OneTimeOrderManagerOption>? managers,
OneTimeOrderManagerOption? selectedManager,
bool? isRapidDraft,
bool? isDataLoaded,
}) {
return OneTimeOrderState(
date: date ?? this.date,
@@ -83,6 +88,7 @@ class OneTimeOrderState extends Equatable {
managers: managers ?? this.managers,
selectedManager: selectedManager ?? this.selectedManager,
isRapidDraft: isRapidDraft ?? this.isRapidDraft,
isDataLoaded: isDataLoaded ?? this.isDataLoaded,
);
}
@@ -187,6 +193,7 @@ class OneTimeOrderState extends Equatable {
managers,
selectedManager,
isRapidDraft,
isDataLoaded,
];
}

View File

@@ -136,7 +136,11 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
? event.vendors.first
: null;
emit(
state.copyWith(vendors: event.vendors, selectedVendor: selectedVendor),
state.copyWith(
vendors: event.vendors,
selectedVendor: selectedVendor,
isDataLoaded: true,
),
);
if (selectedVendor != null) {
await _loadRolesForVendor(selectedVendor.id, emit);

View File

@@ -21,6 +21,7 @@ class PermanentOrderState extends Equatable {
this.roles = const <PermanentOrderRoleOption>[],
this.managers = const <PermanentOrderManagerOption>[],
this.selectedManager,
this.isDataLoaded = false,
});
factory PermanentOrderState.initial() {
@@ -68,6 +69,9 @@ class PermanentOrderState extends Equatable {
final List<PermanentOrderManagerOption> managers;
final PermanentOrderManagerOption? selectedManager;
/// Whether initial data (vendors, hubs) has been fetched from the backend.
final bool isDataLoaded;
PermanentOrderState copyWith({
DateTime? startDate,
List<String>? permanentDays,
@@ -84,6 +88,7 @@ class PermanentOrderState extends Equatable {
List<PermanentOrderRoleOption>? roles,
List<PermanentOrderManagerOption>? managers,
PermanentOrderManagerOption? selectedManager,
bool? isDataLoaded,
}) {
return PermanentOrderState(
startDate: startDate ?? this.startDate,
@@ -101,6 +106,7 @@ class PermanentOrderState extends Equatable {
roles: roles ?? this.roles,
managers: managers ?? this.managers,
selectedManager: selectedManager ?? this.selectedManager,
isDataLoaded: isDataLoaded ?? this.isDataLoaded,
);
}
@@ -186,6 +192,7 @@ class PermanentOrderState extends Equatable {
roles,
managers,
selectedManager,
isDataLoaded,
];
}

View File

@@ -149,7 +149,11 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
? event.vendors.first
: null;
emit(
state.copyWith(vendors: event.vendors, selectedVendor: selectedVendor),
state.copyWith(
vendors: event.vendors,
selectedVendor: selectedVendor,
isDataLoaded: true,
),
);
if (selectedVendor != null) {
await _loadRolesForVendor(selectedVendor.id, emit);

View File

@@ -23,6 +23,7 @@ class RecurringOrderState extends Equatable {
this.roles = const <RecurringOrderRoleOption>[],
this.managers = const <RecurringOrderManagerOption>[],
this.selectedManager,
this.isDataLoaded = false,
});
factory RecurringOrderState.initial() {
@@ -72,6 +73,9 @@ class RecurringOrderState extends Equatable {
final List<RecurringOrderManagerOption> managers;
final RecurringOrderManagerOption? selectedManager;
/// Whether initial data (vendors, hubs) has been fetched from the backend.
final bool isDataLoaded;
RecurringOrderState copyWith({
DateTime? startDate,
DateTime? endDate,
@@ -89,6 +93,7 @@ class RecurringOrderState extends Equatable {
List<RecurringOrderRoleOption>? roles,
List<RecurringOrderManagerOption>? managers,
RecurringOrderManagerOption? selectedManager,
bool? isDataLoaded,
}) {
return RecurringOrderState(
startDate: startDate ?? this.startDate,
@@ -107,6 +112,7 @@ class RecurringOrderState extends Equatable {
roles: roles ?? this.roles,
managers: managers ?? this.managers,
selectedManager: selectedManager ?? this.selectedManager,
isDataLoaded: isDataLoaded ?? this.isDataLoaded,
);
}
@@ -214,6 +220,7 @@ class RecurringOrderState extends Equatable {
roles,
managers,
selectedManager,
isDataLoaded,
];
}

View File

@@ -44,6 +44,7 @@ class OneTimeOrderPage extends StatelessWidget {
);
return OneTimeOrderView(
isDataLoaded: state.isDataLoaded,
status: _mapStatus(state.status),
errorMessage: state.errorMessage,
eventName: state.eventName,

View File

@@ -44,6 +44,7 @@ class PermanentOrderPage extends StatelessWidget {
);
return PermanentOrderView(
isDataLoaded: state.isDataLoaded,
status: _mapStatus(state.status),
errorMessage: state.errorMessage,
eventName: state.eventName,

View File

@@ -43,6 +43,7 @@ class RecurringOrderPage extends StatelessWidget {
);
return RecurringOrderView(
isDataLoaded: state.isDataLoaded,
status: _mapStatus(state.status),
errorMessage: state.errorMessage,
eventName: state.eventName,

View File

@@ -3,6 +3,7 @@ export 'src/presentation/widgets/order_ui_models.dart';
// Shared Widgets
export 'src/presentation/widgets/order_bottom_action_button.dart';
export 'src/presentation/widgets/order_form_skeleton.dart';
// One Time Order Widgets
export 'src/presentation/widgets/one_time_order/one_time_order_date_picker.dart';

View File

@@ -32,7 +32,7 @@ class HubManagerSelector extends StatelessWidget {
children: <Widget>[
Text(
label,
style: UiTypography.body1m.textPrimary,
style: UiTypography.body1r,
),
if (description != null) ...<Widget>[
Text(description!, style: UiTypography.body2r.textSecondary),

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart';
import '../order_bottom_action_button.dart';
import '../order_form_skeleton.dart';
import '../order_ui_models.dart';
import 'one_time_order_form.dart';
import 'one_time_order_success_view.dart';
@@ -37,6 +38,7 @@ class OneTimeOrderView extends StatelessWidget {
required this.onBack,
this.title,
this.subtitle,
this.isDataLoaded = true,
super.key,
});
@@ -56,6 +58,9 @@ class OneTimeOrderView extends StatelessWidget {
final String? title;
final String? subtitle;
/// Whether initial data (vendors, hubs) has been fetched from the backend.
final bool isDataLoaded;
final ValueChanged<String> onEventNameChanged;
final ValueChanged<Vendor> onVendorChanged;
final ValueChanged<DateTime> onDateChanged;
@@ -81,7 +86,12 @@ class OneTimeOrderView extends StatelessWidget {
context,
message: translateErrorKey(errorMessage!),
type: UiSnackbarType.error,
margin: const EdgeInsets.only(bottom: 140, left: 16, right: 16),
// bottom: 140 clears the bottom navigation bar area
margin: const EdgeInsets.only(
bottom: 140,
left: UiConstants.space4,
right: UiConstants.space4,
),
);
});
}
@@ -111,6 +121,10 @@ class OneTimeOrderView extends StatelessWidget {
BuildContext context,
TranslationsClientCreateOrderOneTimeEn labels,
) {
if (!isDataLoaded) {
return const OrderFormSkeleton();
}
if (vendors.isEmpty && status != OrderFormStatus.loading) {
return Column(
children: <Widget>[
@@ -126,12 +140,12 @@ class OneTimeOrderView extends StatelessWidget {
),
const SizedBox(height: UiConstants.space4),
Text(
'No Vendors Available',
t.client_create_order.no_vendors_title,
style: UiTypography.headline3m.textPrimary,
),
const SizedBox(height: UiConstants.space2),
Text(
'There are no staffing vendors associated with your account.',
t.client_create_order.no_vendors_description,
style: UiTypography.body2r.textSecondary,
textAlign: TextAlign.center,
),

View File

@@ -0,0 +1,144 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer skeleton that mimics the order creation form layout.
///
/// Displayed while initial data (vendors, hubs, roles) is being fetched.
/// Renders placeholder shapes for the text input, dropdowns, date picker,
/// hub manager section, and one position card.
class OrderFormSkeleton extends StatelessWidget {
/// Creates an [OrderFormSkeleton].
const OrderFormSkeleton({super.key});
@override
Widget build(BuildContext context) {
return UiShimmer(
child: ListView(
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.all(UiConstants.space5),
children: <Widget>[
_buildLabelPlaceholder(),
const SizedBox(height: UiConstants.space2),
_buildTextFieldPlaceholder(),
const SizedBox(height: UiConstants.space4),
_buildLabelPlaceholder(),
const SizedBox(height: UiConstants.space2),
_buildDropdownPlaceholder(),
const SizedBox(height: UiConstants.space4),
_buildLabelPlaceholder(),
const SizedBox(height: UiConstants.space2),
_buildDropdownPlaceholder(),
const SizedBox(height: UiConstants.space4),
_buildLabelPlaceholder(),
const SizedBox(height: UiConstants.space2),
_buildDropdownPlaceholder(),
const SizedBox(height: UiConstants.space4),
_buildHubManagerPlaceholder(),
const SizedBox(height: UiConstants.space6),
_buildSectionHeaderPlaceholder(),
const SizedBox(height: UiConstants.space3),
_buildPositionCardPlaceholder(),
],
),
);
}
/// Small label placeholder above each field.
Widget _buildLabelPlaceholder() {
return const Align(
alignment: Alignment.centerLeft,
child: UiShimmerLine(width: 100, height: 12),
);
}
/// Full-width text input placeholder.
Widget _buildTextFieldPlaceholder() {
return const UiShimmerBox(width: double.infinity, height: 48);
}
/// Full-width dropdown selector placeholder.
Widget _buildDropdownPlaceholder() {
return const UiShimmerBox(width: double.infinity, height: 48);
}
/// Hub manager section with label and description lines.
Widget _buildHubManagerPlaceholder() {
return const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UiShimmerLine(width: 140, height: 12),
SizedBox(height: UiConstants.space1),
UiShimmerLine(width: 220, height: 10),
SizedBox(height: UiConstants.space2),
UiShimmerBox(width: double.infinity, height: 48),
],
);
}
/// Section header placeholder with title and action button.
Widget _buildSectionHeaderPlaceholder() {
return const Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
UiShimmerLine(width: 100, height: 16),
UiShimmerBox(width: 90, height: 28),
],
);
}
/// Position card placeholder mimicking role, worker count, and time fields.
Widget _buildPositionCardPlaceholder() {
return Container(
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusMd,
border: Border.all(color: UiColors.border),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
UiShimmerLine(width: 80, height: 14),
UiShimmerCircle(size: 24),
],
),
const SizedBox(height: UiConstants.space3),
const UiShimmerBox(width: double.infinity, height: 44),
const SizedBox(height: UiConstants.space3),
const UiShimmerLine(width: 60, height: 12),
const SizedBox(height: UiConstants.space2),
const UiShimmerBox(width: double.infinity, height: 44),
const SizedBox(height: UiConstants.space3),
Row(
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const <Widget>[
UiShimmerLine(width: 50, height: 12),
SizedBox(height: UiConstants.space2),
UiShimmerBox(width: double.infinity, height: 44),
],
),
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const <Widget>[
UiShimmerLine(width: 50, height: 12),
SizedBox(height: UiConstants.space2),
UiShimmerBox(width: double.infinity, height: 44),
],
),
),
],
),
],
),
);
}
}

View File

@@ -105,20 +105,12 @@ class PermanentOrderForm extends StatelessWidget {
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderPermanentEn labels =
t.client_create_order.permanent;
final TranslationsClientCreateOrderOneTimeEn oneTimeLabels =
t.client_create_order.one_time;
return ListView(
padding: const EdgeInsets.all(UiConstants.space5),
children: <Widget>[
Text(
labels.title,
style: UiTypography.headline3m.textPrimary,
),
const SizedBox(height: UiConstants.space4),
PermanentOrderEventNameInput(
label: 'ORDER NAME',
value: eventName,

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart' show Vendor;
import '../order_bottom_action_button.dart';
import '../order_form_skeleton.dart';
import '../order_ui_models.dart';
import 'permanent_order_form.dart';
import 'permanent_order_success_view.dart';
@@ -37,9 +38,12 @@ class PermanentOrderView extends StatelessWidget {
required this.onSubmit,
required this.onDone,
required this.onBack,
this.isDataLoaded = true,
super.key,
});
/// Whether initial data (vendors, hubs) has been fetched from the backend.
final bool isDataLoaded;
final OrderFormStatus status;
final String? errorMessage;
final String eventName;
@@ -82,7 +86,12 @@ class PermanentOrderView extends StatelessWidget {
context,
message: translateErrorKey(errorMessage!),
type: UiSnackbarType.error,
margin: const EdgeInsets.only(bottom: 140, left: 16, right: 16),
// bottom: 140 clears the bottom navigation bar area
margin: const EdgeInsets.only(
bottom: 140,
left: UiConstants.space4,
right: UiConstants.space4,
),
);
});
}
@@ -113,6 +122,10 @@ class PermanentOrderView extends StatelessWidget {
TranslationsClientCreateOrderPermanentEn labels,
TranslationsClientCreateOrderOneTimeEn oneTimeLabels,
) {
if (!isDataLoaded) {
return const OrderFormSkeleton();
}
if (vendors.isEmpty && status != OrderFormStatus.loading) {
return Column(
children: <Widget>[
@@ -128,12 +141,12 @@ class PermanentOrderView extends StatelessWidget {
),
const SizedBox(height: UiConstants.space4),
Text(
'No Vendors Available',
t.client_create_order.no_vendors_title,
style: UiTypography.headline3m.textPrimary,
),
const SizedBox(height: UiConstants.space2),
Text(
'There are no staffing vendors associated with your account.',
t.client_create_order.no_vendors_description,
style: UiTypography.body2r.textSecondary,
textAlign: TextAlign.center,
),

View File

@@ -100,7 +100,7 @@ class RecurringOrderForm extends StatelessWidget {
/// Called when a position at [index] is updated with new values.
final void Function(int index, OrderPositionUiModel position)
onPositionUpdated;
onPositionUpdated;
/// Called when a position at [index] is removed.
final void Function(int index) onPositionRemoved;
@@ -113,20 +113,12 @@ class RecurringOrderForm extends StatelessWidget {
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderRecurringEn labels =
t.client_create_order.recurring;
final TranslationsClientCreateOrderOneTimeEn oneTimeLabels =
t.client_create_order.one_time;
return ListView(
padding: const EdgeInsets.all(UiConstants.space5),
children: <Widget>[
Text(
labels.title,
style: UiTypography.headline3m.textPrimary,
),
const SizedBox(height: UiConstants.space4),
RecurringOrderEventNameInput(
label: 'ORDER NAME',
value: eventName,
@@ -222,16 +214,13 @@ class RecurringOrderForm extends StatelessWidget {
items: hubs.map((OrderHubUiModel hub) {
return DropdownMenuItem<OrderHubUiModel>(
value: hub,
child: Text(
hub.name,
style: UiTypography.body2m.textPrimary,
),
child: Text(hub.name, style: UiTypography.body2m.textPrimary),
);
}).toList(),
),
),
),
const SizedBox(height: UiConstants.space4),
const SizedBox(height: UiConstants.space6),
HubManagerSelector(
label: oneTimeLabels.hub_manager_label,

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart' show Vendor;
import '../order_bottom_action_button.dart';
import '../order_form_skeleton.dart';
import '../order_ui_models.dart';
import 'recurring_order_form.dart';
import 'recurring_order_success_view.dart';
@@ -39,9 +40,12 @@ class RecurringOrderView extends StatelessWidget {
required this.onSubmit,
required this.onDone,
required this.onBack,
this.isDataLoaded = true,
super.key,
});
/// Whether initial data (vendors, hubs) has been fetched from the backend.
final bool isDataLoaded;
final OrderFormStatus status;
final String? errorMessage;
final String eventName;
@@ -89,7 +93,12 @@ class RecurringOrderView extends StatelessWidget {
context,
message: message,
type: UiSnackbarType.error,
margin: const EdgeInsets.only(bottom: 140, left: 16, right: 16),
// bottom: 140 clears the bottom navigation bar area
margin: const EdgeInsets.only(
bottom: 140,
left: UiConstants.space4,
right: UiConstants.space4,
),
);
});
}
@@ -120,6 +129,10 @@ class RecurringOrderView extends StatelessWidget {
TranslationsClientCreateOrderRecurringEn labels,
TranslationsClientCreateOrderOneTimeEn oneTimeLabels,
) {
if (!isDataLoaded) {
return const OrderFormSkeleton();
}
if (vendors.isEmpty && status != OrderFormStatus.loading) {
return Column(
children: <Widget>[
@@ -135,12 +148,12 @@ class RecurringOrderView extends StatelessWidget {
),
const SizedBox(height: UiConstants.space4),
Text(
'No Vendors Available',
t.client_create_order.no_vendors_title,
style: UiTypography.headline3m.textPrimary,
),
const SizedBox(height: UiConstants.space2),
Text(
'There are no staffing vendors associated with your account.',
t.client_create_order.no_vendors_description,
style: UiTypography.body2r.textSecondary,
textAlign: TextAlign.center,
),

View File

@@ -11,6 +11,7 @@ import '../widgets/view_orders_header.dart';
import '../widgets/view_orders_empty_state.dart';
import '../widgets/view_orders_error_state.dart';
import '../widgets/view_orders_list.dart';
import '../widgets/view_orders_page_skeleton.dart';
/// The main page for viewing client orders.
///
@@ -101,20 +102,26 @@ class _ViewOrdersViewState extends State<ViewOrdersView> {
// Content List
Expanded(
child: state.status == ViewOrdersStatus.failure
? ViewOrdersErrorState(
errorMessage: state.errorMessage,
selectedDate: state.selectedDate,
onRetry: () => BlocProvider.of<ViewOrdersCubit>(
context,
).jumpToDate(state.selectedDate ?? DateTime.now()),
)
: filteredOrders.isEmpty
? ViewOrdersEmptyState(selectedDate: state.selectedDate)
: ViewOrdersList(
orders: filteredOrders,
filterTab: state.filterTab,
),
child: switch (state.status) {
ViewOrdersStatus.loading ||
ViewOrdersStatus.initial =>
const ViewOrdersPageSkeleton(),
ViewOrdersStatus.failure => ViewOrdersErrorState(
errorMessage: state.errorMessage,
selectedDate: state.selectedDate,
onRetry: () => BlocProvider.of<ViewOrdersCubit>(
context,
).jumpToDate(state.selectedDate ?? DateTime.now()),
),
ViewOrdersStatus.success => filteredOrders.isEmpty
? ViewOrdersEmptyState(
selectedDate: state.selectedDate,
)
: ViewOrdersList(
orders: filteredOrders,
filterTab: state.filterTab,
),
},
),
],
),

View File

@@ -0,0 +1 @@
export 'view_orders_page_skeleton/index.dart';

View File

@@ -0,0 +1,5 @@
export 'order_card_skeleton.dart';
export 'section_header_skeleton.dart';
export 'stat_divider_skeleton.dart';
export 'stat_item_skeleton.dart';
export 'view_orders_page_skeleton.dart';

View File

@@ -0,0 +1,127 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'stat_divider_skeleton.dart';
import 'stat_item_skeleton.dart';
/// Shimmer placeholder for a single order card.
class OrderCardSkeleton extends StatelessWidget {
/// Creates an [OrderCardSkeleton].
const OrderCardSkeleton({super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: UiColors.border, width: 0.5),
borderRadius: UiConstants.radiusLg,
),
child: Padding(
padding: const EdgeInsets.all(UiConstants.space5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// Status and type badges
Row(
children: <Widget>[
UiShimmerBox(
width: 80,
height: 22,
borderRadius: UiConstants.radiusSm,
),
const SizedBox(width: UiConstants.space2),
UiShimmerBox(
width: 72,
height: 22,
borderRadius: UiConstants.radiusSm,
),
],
),
const SizedBox(height: UiConstants.space3),
// Title line
const UiShimmerLine(width: 200, height: 18),
const SizedBox(height: UiConstants.space2),
// Event name line
const UiShimmerLine(width: 160, height: 14),
const SizedBox(height: UiConstants.space4),
// Location lines
const Row(
children: <Widget>[
UiShimmerCircle(size: 14),
SizedBox(width: UiConstants.space2),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UiShimmerLine(width: 180, height: 12),
SizedBox(height: UiConstants.space1),
UiShimmerLine(width: 140, height: 10),
],
),
),
],
),
const SizedBox(height: UiConstants.space4),
const Divider(height: 1, color: UiColors.border),
const SizedBox(height: UiConstants.space4),
// Stats row (cost / hours / workers)
const Padding(
padding: EdgeInsets.symmetric(
horizontal: UiConstants.space4,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
StatItemSkeleton(),
StatDividerSkeleton(),
StatItemSkeleton(),
StatDividerSkeleton(),
StatItemSkeleton(),
],
),
),
const SizedBox(height: UiConstants.space5),
// Time boxes (clock in / clock out)
Row(
children: <Widget>[
Expanded(child: _timeBoxSkeleton()),
const SizedBox(width: UiConstants.space3),
Expanded(child: _timeBoxSkeleton()),
],
),
const SizedBox(height: UiConstants.space4),
// Coverage progress bar
const UiShimmerLine(height: 8),
],
),
),
);
}
/// Builds a placeholder for a time display box (clock-in / clock-out).
Widget _timeBoxSkeleton() {
return Container(
padding: const EdgeInsets.all(UiConstants.space3),
decoration: BoxDecoration(
border: Border.all(color: UiColors.border, width: 0.5),
borderRadius: UiConstants.radiusLg,
),
child: const Column(
children: <Widget>[
UiShimmerLine(width: 60, height: 10),
SizedBox(height: UiConstants.space2),
UiShimmerLine(width: 80, height: 16),
],
),
);
}
}

View File

@@ -0,0 +1,24 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer placeholder for the section header row (dot + title + count).
class SectionHeaderSkeleton extends StatelessWidget {
/// Creates a [SectionHeaderSkeleton].
const SectionHeaderSkeleton({super.key});
@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.only(bottom: UiConstants.space3),
child: Row(
children: <Widget>[
UiShimmerCircle(size: 8),
SizedBox(width: UiConstants.space2),
UiShimmerLine(width: 100, height: 14),
SizedBox(width: UiConstants.space1),
UiShimmerLine(width: 24, height: 14),
],
),
);
}
}

View File

@@ -0,0 +1,17 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer placeholder for the vertical stat divider.
class StatDividerSkeleton extends StatelessWidget {
/// Creates a [StatDividerSkeleton].
const StatDividerSkeleton({super.key});
@override
Widget build(BuildContext context) {
return const UiShimmerBox(
width: 1,
height: 24,
borderRadius: BorderRadius.zero,
);
}
}

View File

@@ -0,0 +1,20 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer placeholder for a single stat item (icon + value + label).
class StatItemSkeleton extends StatelessWidget {
/// Creates a [StatItemSkeleton].
const StatItemSkeleton({super.key});
@override
Widget build(BuildContext context) {
return const Column(
spacing: UiConstants.space1,
children: <Widget>[
UiShimmerCircle(size: 14),
UiShimmerLine(width: 32, height: 16),
UiShimmerLine(width: 40, height: 10),
],
);
}
}

View File

@@ -0,0 +1,41 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'order_card_skeleton.dart';
import 'section_header_skeleton.dart';
/// Shimmer loading skeleton for the View Orders page.
///
/// Mimics the loaded layout: a section header followed by a list of order
/// card placeholders, each containing badge, title, location, stats, time
/// boxes, and a coverage progress bar.
class ViewOrdersPageSkeleton extends StatelessWidget {
/// Creates a [ViewOrdersPageSkeleton].
const ViewOrdersPageSkeleton({super.key});
@override
Widget build(BuildContext context) {
return UiShimmer(
child: ListView(
padding: const EdgeInsets.fromLTRB(
UiConstants.space5,
UiConstants.space4,
UiConstants.space5,
// Extra bottom padding for bottom navigation clearance.
UiConstants.space24,
),
children: <Widget>[
// Section header placeholder (dot + title + count)
const SectionHeaderSkeleton(),
// Order card placeholders
...List<Widget>.generate(3, (int index) {
return const Padding(
padding: EdgeInsets.only(bottom: UiConstants.space3),
child: OrderCardSkeleton(),
);
}),
],
),
);
}
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'data/repositories/view_orders_repository_impl.dart';
@@ -33,7 +34,7 @@ class ViewOrdersModule extends Module {
@override
void routes(RouteManager r) {
r.child(
'/',
ClientPaths.childRoute(ClientPaths.orders, ClientPaths.orders),
child: (BuildContext context) {
final Object? args = Modular.args.data;
DateTime? initialDate;

View File

@@ -10,6 +10,8 @@ import 'package:intl/intl.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../widgets/report_detail_skeleton.dart';
class CoverageReportPage extends StatefulWidget {
const CoverageReportPage({super.key});
@@ -30,7 +32,7 @@ class _CoverageReportPageState extends State<CoverageReportPage> {
body: BlocBuilder<CoverageBloc, CoverageState>(
builder: (BuildContext context, CoverageState state) {
if (state is CoverageLoading) {
return const Center(child: CircularProgressIndicator());
return const ReportDetailSkeleton();
}
if (state is CoverageError) {

View File

@@ -10,6 +10,8 @@ import 'package:intl/intl.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../widgets/report_detail_skeleton.dart';
class DailyOpsReportPage extends StatefulWidget {
const DailyOpsReportPage({super.key});
@@ -57,7 +59,7 @@ class _DailyOpsReportPageState extends State<DailyOpsReportPage> {
body: BlocBuilder<DailyOpsBloc, DailyOpsState>(
builder: (BuildContext context, DailyOpsState state) {
if (state is DailyOpsLoading) {
return const Center(child: CircularProgressIndicator());
return const ReportDetailSkeleton();
}
if (state is DailyOpsError) {

View File

@@ -12,6 +12,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:intl/intl.dart';
import '../widgets/report_detail_skeleton.dart';
class ForecastReportPage extends StatefulWidget {
const ForecastReportPage({super.key});
@@ -32,7 +34,7 @@ class _ForecastReportPageState extends State<ForecastReportPage> {
body: BlocBuilder<ForecastBloc, ForecastState>(
builder: (BuildContext context, ForecastState state) {
if (state is ForecastLoading) {
return const Center(child: CircularProgressIndicator());
return const ReportDetailSkeleton();
}
if (state is ForecastError) {

View File

@@ -11,6 +11,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:intl/intl.dart';
import '../widgets/report_detail_skeleton.dart';
class NoShowReportPage extends StatefulWidget {
const NoShowReportPage({super.key});
@@ -31,7 +33,7 @@ class _NoShowReportPageState extends State<NoShowReportPage> {
body: BlocBuilder<NoShowBloc, NoShowState>(
builder: (BuildContext context, NoShowState state) {
if (state is NoShowLoading) {
return const Center(child: CircularProgressIndicator());
return const ReportDetailSkeleton();
}
if (state is NoShowError) {

View File

@@ -9,6 +9,8 @@ import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../widgets/report_detail_skeleton.dart';
class PerformanceReportPage extends StatefulWidget {
const PerformanceReportPage({super.key});
@@ -29,7 +31,7 @@ class _PerformanceReportPageState extends State<PerformanceReportPage> {
body: BlocBuilder<PerformanceBloc, PerformanceState>(
builder: (BuildContext context, PerformanceState state) {
if (state is PerformanceLoading) {
return const Center(child: CircularProgressIndicator());
return const ReportDetailSkeleton();
}
if (state is PerformanceError) {

View File

@@ -11,6 +11,8 @@ import 'package:intl/intl.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../widgets/report_detail_skeleton.dart';
class SpendReportPage extends StatefulWidget {
const SpendReportPage({super.key});
@@ -42,7 +44,7 @@ class _SpendReportPageState extends State<SpendReportPage> {
body: BlocBuilder<SpendBloc, SpendState>(
builder: (BuildContext context, SpendState state) {
if (state is SpendLoading) {
return const Center(child: CircularProgressIndicator());
return const ReportDetailSkeleton();
}
if (state is SpendError) {

View File

@@ -0,0 +1,156 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer loading skeleton for individual report detail pages.
///
/// Shows a header area, two summary stat cards, a chart placeholder,
/// and a breakdown list. Used by spend, coverage, no-show, forecast,
/// daily ops, and performance report pages.
class ReportDetailSkeleton extends StatelessWidget {
/// Creates a [ReportDetailSkeleton].
const ReportDetailSkeleton({super.key});
@override
Widget build(BuildContext context) {
return UiShimmer(
child: SingleChildScrollView(
child: Column(
children: [
// Header area (matches the blue header with back button + title)
Container(
padding: const EdgeInsets.only(
top: 60,
left: UiConstants.space5,
right: UiConstants.space5,
bottom: UiConstants.space10,
),
color: UiColors.primary,
child: Row(
children: [
const UiShimmerCircle(size: UiConstants.space10),
const SizedBox(width: UiConstants.space3),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
UiShimmerBox(
width: 140,
height: 18,
borderRadius: UiConstants.radiusSm,
),
const SizedBox(height: UiConstants.space2),
UiShimmerBox(
width: 100,
height: 12,
borderRadius: UiConstants.radiusSm,
),
],
),
],
),
),
// Content pulled up to overlap header
Transform.translate(
offset: const Offset(0, -40),
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: UiConstants.space5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Summary stat cards row
const Row(
children: [
Expanded(child: UiShimmerStatsCard()),
SizedBox(width: UiConstants.space3),
Expanded(child: UiShimmerStatsCard()),
],
),
const SizedBox(height: UiConstants.space6),
// Chart placeholder
Container(
height: 280,
padding: const EdgeInsets.all(UiConstants.space5),
decoration: BoxDecoration(
border: Border.all(color: UiColors.border),
borderRadius: UiConstants.radiusXl,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const UiShimmerLine(width: 140, height: 14),
const SizedBox(height: UiConstants.space8),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(7, (int index) {
// Varying bar heights for visual interest
final double height =
40.0 + (index * 17 % 120);
return UiShimmerBox(
width: 12,
height: height,
borderRadius: UiConstants.radiusSm,
);
}),
),
),
const SizedBox(height: UiConstants.space3),
const UiShimmerLine(height: 10),
],
),
),
const SizedBox(height: UiConstants.space6),
// Breakdown section
Container(
padding: const EdgeInsets.all(UiConstants.space5),
decoration: BoxDecoration(
border: Border.all(color: UiColors.border),
borderRadius: UiConstants.radiusXl,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const UiShimmerLine(width: 160, height: 14),
const SizedBox(height: UiConstants.space6),
...List.generate(3, (int index) {
return Padding(
padding: const EdgeInsets.only(
bottom: UiConstants.space5,
),
child: Column(
children: [
const Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
UiShimmerLine(width: 100, height: 12),
UiShimmerLine(width: 60, height: 12),
],
),
const SizedBox(height: UiConstants.space2),
UiShimmerBox(
width: double.infinity,
height: 6,
borderRadius: UiConstants.radiusSm,
),
],
),
);
}),
],
),
),
],
),
),
),
],
),
),
);
}
}

View File

@@ -1,5 +1,6 @@
export 'metric_card.dart';
export 'metrics_grid.dart';
export 'metrics_grid_skeleton.dart';
export 'quick_reports_section.dart';
export 'report_card.dart';
export 'reports_header.dart';

View File

@@ -8,6 +8,7 @@ import 'package:intl/intl.dart';
import 'package:krow_domain/krow_domain.dart';
import 'metric_card.dart';
import 'metrics_grid_skeleton.dart';
/// A grid of key metrics driven by the ReportsSummaryBloc.
///
@@ -29,10 +30,7 @@ class MetricsGrid extends StatelessWidget {
builder: (BuildContext context, ReportsSummaryState state) {
// Loading or Initial State
if (state is ReportsSummaryLoading || state is ReportsSummaryInitial) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 32),
child: Center(child: CircularProgressIndicator()),
);
return const MetricsGridSkeleton();
}
// Error State

View File

@@ -0,0 +1 @@
export 'metrics_grid_skeleton/index.dart';

View File

@@ -0,0 +1,2 @@
export 'metric_card_skeleton.dart';
export 'metrics_grid_skeleton.dart';

View File

@@ -0,0 +1,45 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer placeholder for a single metric card.
class MetricCardSkeleton extends StatelessWidget {
/// Creates a [MetricCardSkeleton].
const MetricCardSkeleton({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: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Icon + label row
Row(
children: [
const UiShimmerCircle(size: UiConstants.space6),
const SizedBox(width: UiConstants.space2),
const Expanded(
child: UiShimmerLine(width: 60, height: 10),
),
],
),
const Spacer(),
// Value
const UiShimmerLine(width: 80, height: 22),
const SizedBox(height: UiConstants.space2),
// Badge
UiShimmerBox(
width: 60,
height: 20,
borderRadius: UiConstants.radiusSm,
),
],
),
);
}
}

View File

@@ -0,0 +1,31 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'metric_card_skeleton.dart';
/// Shimmer loading skeleton for the reports metrics grid.
///
/// Shows a 2-column grid of 6 placeholder cards matching the [MetricsGrid]
/// loaded layout.
class MetricsGridSkeleton extends StatelessWidget {
/// Creates a [MetricsGridSkeleton].
const MetricsGridSkeleton({super.key});
@override
Widget build(BuildContext context) {
return UiShimmer(
child: GridView.count(
padding: const EdgeInsets.symmetric(vertical: UiConstants.space6),
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
mainAxisSpacing: UiConstants.space3,
crossAxisSpacing: UiConstants.space3,
childAspectRatio: 1.32,
children: List.generate(6, (int index) {
return const MetricCardSkeleton();
}),
),
);
}
}

View File

@@ -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: [

View File

@@ -0,0 +1 @@
export 'availability_page_skeleton/index.dart';

View File

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

View File

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

View File

@@ -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';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
export 'home_page_skeleton/index.dart';

Some files were not shown because too many files have changed in this diff Show More