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:
22
.claude/agent-memory/mobile-architecture-reviewer/MEMORY.md
Normal file
22
.claude/agent-memory/mobile-architecture-reviewer/MEMORY.md
Normal 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
|
||||
55
.claude/agent-memory/mobile-builder/MEMORY.md
Normal file
55
.claude/agent-memory/mobile-builder/MEMORY.md
Normal 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
|
||||
@@ -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
3
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export 'billing_page_skeleton/index.dart';
|
||||
@@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export 'billing_page_skeleton.dart';
|
||||
export 'breakdown_row_skeleton.dart';
|
||||
export 'invoice_card_skeleton.dart';
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export 'coverage_page_skeleton/index.dart';
|
||||
@@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export 'coverage_page_skeleton.dart';
|
||||
export 'shift_card_skeleton.dart';
|
||||
@@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ class OneTimeOrderPage extends StatelessWidget {
|
||||
);
|
||||
|
||||
return OneTimeOrderView(
|
||||
isDataLoaded: state.isDataLoaded,
|
||||
status: _mapStatus(state.status),
|
||||
errorMessage: state.errorMessage,
|
||||
eventName: state.eventName,
|
||||
|
||||
@@ -44,6 +44,7 @@ class PermanentOrderPage extends StatelessWidget {
|
||||
);
|
||||
|
||||
return PermanentOrderView(
|
||||
isDataLoaded: state.isDataLoaded,
|
||||
status: _mapStatus(state.status),
|
||||
errorMessage: state.errorMessage,
|
||||
eventName: state.eventName,
|
||||
|
||||
@@ -43,6 +43,7 @@ class RecurringOrderPage extends StatelessWidget {
|
||||
);
|
||||
|
||||
return RecurringOrderView(
|
||||
isDataLoaded: state.isDataLoaded,
|
||||
status: _mapStatus(state.status),
|
||||
errorMessage: state.errorMessage,
|
||||
eventName: state.eventName,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export 'view_orders_page_skeleton/index.dart';
|
||||
@@ -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';
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export 'metrics_grid_skeleton/index.dart';
|
||||
@@ -0,0 +1,2 @@
|
||||
export 'metric_card_skeleton.dart';
|
||||
export 'metrics_grid_skeleton.dart';
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
import '../blocs/availability_bloc.dart';
|
||||
import '../blocs/availability_event.dart';
|
||||
import '../blocs/availability_state.dart';
|
||||
import '../widgets/availability_page_skeleton/availability_page_skeleton.dart';
|
||||
|
||||
class AvailabilityPage extends StatefulWidget {
|
||||
const AvailabilityPage({super.key});
|
||||
@@ -72,7 +73,7 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
child: BlocBuilder<AvailabilityBloc, AvailabilityState>(
|
||||
builder: (context, state) {
|
||||
if (state is AvailabilityLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
return const AvailabilityPageSkeleton();
|
||||
} else if (state is AvailabilityLoaded) {
|
||||
return Stack(
|
||||
children: [
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export 'availability_page_skeleton/index.dart';
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'day_availability_skeleton.dart';
|
||||
import 'info_card_skeleton.dart';
|
||||
import 'quick_set_skeleton.dart';
|
||||
import 'week_navigation_skeleton.dart';
|
||||
|
||||
/// Shimmer loading skeleton for the availability page.
|
||||
///
|
||||
/// Mimics the loaded layout: quick-set buttons, week navigation calendar,
|
||||
/// selected day detail with time-slot rows, and an info card.
|
||||
class AvailabilityPageSkeleton extends StatelessWidget {
|
||||
/// Creates an [AvailabilityPageSkeleton].
|
||||
const AvailabilityPageSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiShimmer(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
),
|
||||
child: Column(
|
||||
spacing: UiConstants.space6,
|
||||
children: const [
|
||||
QuickSetSkeleton(),
|
||||
WeekNavigationSkeleton(),
|
||||
DayAvailabilitySkeleton(),
|
||||
InfoCardSkeleton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for the selected day detail card (header + time slot rows).
|
||||
class DayAvailabilitySkeleton extends StatelessWidget {
|
||||
/// Creates a [DayAvailabilitySkeleton].
|
||||
const DayAvailabilitySkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Header: date text + toggle placeholder
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const [
|
||||
UiShimmerLine(width: 160, height: 16),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
UiShimmerLine(width: 80, height: 12),
|
||||
],
|
||||
),
|
||||
UiShimmerBox(
|
||||
width: 48,
|
||||
height: 28,
|
||||
borderRadius: UiConstants.radiusFull,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
// 3 time-slot rows (morning, afternoon, evening)
|
||||
..._buildSlotPlaceholders(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Generates 3 time-slot shimmer rows.
|
||||
List<Widget> _buildSlotPlaceholders() {
|
||||
return List.generate(3, (index) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: index < 2 ? UiConstants.space3 : 0,
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Icon placeholder
|
||||
UiShimmerBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius:
|
||||
UiConstants.radiusLg,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
// Text lines
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
UiShimmerLine(width: 80, height: 14),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
UiShimmerLine(width: 120, height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Checkbox circle
|
||||
const UiShimmerCircle(size: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export 'availability_page_skeleton.dart';
|
||||
export 'day_availability_skeleton.dart';
|
||||
export 'info_card_skeleton.dart';
|
||||
export 'quick_set_skeleton.dart';
|
||||
export 'week_navigation_skeleton.dart';
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for the info card at the bottom (icon + two text lines).
|
||||
class InfoCardSkeleton extends StatelessWidget {
|
||||
/// Creates an [InfoCardSkeleton].
|
||||
const InfoCardSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const UiShimmerCircle(size: 20),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const [
|
||||
UiShimmerLine(width: 140, height: 14),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
UiShimmerLine(height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for the quick-set section (title + 4 action buttons).
|
||||
class QuickSetSkeleton extends StatelessWidget {
|
||||
/// Creates a [QuickSetSkeleton].
|
||||
const QuickSetSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Title line
|
||||
const UiShimmerLine(width: 100, height: 14),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
// Row of 4 button placeholders
|
||||
Row(
|
||||
children: List.generate(4, (index) {
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: index == 0 ? 0 : UiConstants.space1,
|
||||
right: index == 3 ? 0 : UiConstants.space1,
|
||||
),
|
||||
child: UiShimmerBox(
|
||||
width: double.infinity,
|
||||
height: 32,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for the week navigation card (month header + 7 day cells).
|
||||
class WeekNavigationSkeleton extends StatelessWidget {
|
||||
/// Creates a [WeekNavigationSkeleton].
|
||||
const WeekNavigationSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Navigation header: left arrow, month label, right arrow
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: UiConstants.space4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const UiShimmerCircle(size: 32),
|
||||
UiShimmerLine(width: 140, height: 16),
|
||||
const UiShimmerCircle(size: 32),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 7 day cells
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.generate(7, (_) {
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space1),
|
||||
child: UiShimmerBox(
|
||||
width: double.infinity,
|
||||
height: 64,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,12 +17,12 @@ class StaffAvailabilityModule extends Module {
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
// Repository
|
||||
i.add<AvailabilityRepository>(AvailabilityRepositoryImpl.new);
|
||||
i.addLazySingleton<AvailabilityRepository>(AvailabilityRepositoryImpl.new);
|
||||
|
||||
// UseCases
|
||||
i.add(GetWeeklyAvailabilityUseCase.new);
|
||||
i.add(UpdateDayAvailabilityUseCase.new);
|
||||
i.add(ApplyQuickSetUseCase.new);
|
||||
i.addLazySingleton(GetWeeklyAvailabilityUseCase.new);
|
||||
i.addLazySingleton(UpdateDayAvailabilityUseCase.new);
|
||||
i.addLazySingleton(ApplyQuickSetUseCase.new);
|
||||
|
||||
// BLoC
|
||||
i.add(AvailabilityBloc.new);
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
import '../bloc/clock_in_bloc.dart';
|
||||
import '../bloc/clock_in_event.dart';
|
||||
import '../bloc/clock_in_state.dart';
|
||||
import '../widgets/clock_in_page_skeleton/clock_in_page_skeleton.dart';
|
||||
import '../widgets/commute_tracker.dart';
|
||||
import '../widgets/date_selector.dart';
|
||||
import '../widgets/lunch_break_modal.dart';
|
||||
@@ -52,8 +53,9 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
builder: (BuildContext context, ClockInState state) {
|
||||
if (state.status == ClockInStatus.loading &&
|
||||
state.todayShifts.isEmpty) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
return Scaffold(
|
||||
appBar: UiAppBar(title: i18n.title, showBackButton: false),
|
||||
body: const SafeArea(child: ClockInPageSkeleton()),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for the "Your Activity" section header text.
|
||||
class ActivityHeaderSkeleton extends StatelessWidget {
|
||||
/// Creates a shimmer line matching the activity header.
|
||||
const ActivityHeaderSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: UiShimmerLine(width: 120, height: 18),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'activity_header_skeleton.dart';
|
||||
import 'date_selector_skeleton.dart';
|
||||
import 'shift_card_skeleton.dart';
|
||||
import 'swipe_action_skeleton.dart';
|
||||
|
||||
/// Full-page shimmer skeleton shown while clock-in data loads.
|
||||
///
|
||||
/// Mirrors the loaded [ClockInPage] layout: date selector, activity header,
|
||||
/// two shift cards, and the swipe-to-check-in bar.
|
||||
class ClockInPageSkeleton extends StatelessWidget {
|
||||
/// Creates the clock-in page shimmer skeleton.
|
||||
const ClockInPageSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiShimmer(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: UiConstants.space24,
|
||||
top: UiConstants.space6,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const <Widget>[
|
||||
// Date selector row
|
||||
DateSelectorSkeleton(),
|
||||
SizedBox(height: UiConstants.space5),
|
||||
|
||||
// "Your Activity" header
|
||||
ActivityHeaderSkeleton(),
|
||||
SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Shift cards (show two placeholders)
|
||||
ShiftCardSkeleton(),
|
||||
ShiftCardSkeleton(),
|
||||
|
||||
// Swipe action bar
|
||||
SwipeActionSkeleton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for the [DateSelector] row of 7 day chips.
|
||||
class DateSelectorSkeleton extends StatelessWidget {
|
||||
/// Creates a shimmer placeholder matching the date selector layout.
|
||||
const DateSelectorSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 80,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List<Widget>.generate(7, (int index) {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space1,
|
||||
),
|
||||
child: UiShimmerBox(
|
||||
width: double.infinity,
|
||||
height: 80,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for a single shift info card.
|
||||
///
|
||||
/// Mirrors the two-column layout: left side has badge, title, and subtitle
|
||||
/// lines; right side has time range and rate lines.
|
||||
class ShiftCardSkeleton extends StatelessWidget {
|
||||
/// Creates a shimmer placeholder for one shift card.
|
||||
const ShiftCardSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: const Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// Left column: badge + title + subtitle
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
UiShimmerLine(width: 80, height: 10),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
UiShimmerLine(width: 160, height: 14),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
UiShimmerLine(width: 200, height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: UiConstants.space3),
|
||||
// Right column: time + rate
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
UiShimmerLine(width: 100, height: 12),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
UiShimmerLine(width: 60, height: 12),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shimmer placeholder for the swipe-to-check-in action area.
|
||||
class SwipeActionSkeleton extends StatelessWidget {
|
||||
/// Creates a shimmer placeholder matching the swipe bar height.
|
||||
const SwipeActionSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiShimmerBox(
|
||||
width: double.infinity,
|
||||
height: 60,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,9 @@ import 'package:staff_home/src/presentation/blocs/home/home_cubit.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/home_page/benefits_section.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/home_page/full_width_divider.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/home_page/home_header.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/home_page/staff_home_header_skeleton.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/home_page/placeholder_banner.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/home_page/home_page_skeleton.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/home_page/quick_actions_section.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/home_page/recommended_shifts_section.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/home_page/todays_shifts_section.dart';
|
||||
@@ -47,8 +49,13 @@ class WorkerHomePage extends StatelessWidget {
|
||||
children: [
|
||||
BlocBuilder<HomeCubit, HomeState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.staffName != current.staffName,
|
||||
previous.staffName != current.staffName ||
|
||||
previous.status != current.status,
|
||||
builder: (context, state) {
|
||||
if (state.status == HomeStatus.initial ||
|
||||
state.status == HomeStatus.loading) {
|
||||
return const StaffHomeHeaderSkeleton();
|
||||
}
|
||||
return HomeHeader(userName: state.staffName);
|
||||
},
|
||||
),
|
||||
@@ -59,8 +66,14 @@ class WorkerHomePage extends StatelessWidget {
|
||||
),
|
||||
child: BlocBuilder<HomeCubit, HomeState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.status != current.status ||
|
||||
previous.isProfileComplete != current.isProfileComplete,
|
||||
builder: (context, state) {
|
||||
if (state.status == HomeStatus.loading ||
|
||||
state.status == HomeStatus.initial) {
|
||||
return const HomePageSkeleton();
|
||||
}
|
||||
|
||||
if (!state.isProfileComplete) {
|
||||
return SizedBox(
|
||||
height: MediaQuery.of(context).size.height - 300,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user