feat: Add shimmer loading skeletons for various pages and components
- Implemented `ClientHomePageSkeleton` for the client home page to display a loading state with shimmer effects. - Created `OrderFormSkeleton` to mimic the layout of the order creation form while data is being fetched. - Added `ViewOrdersPageSkeleton` to represent the loading state of the view orders page with placeholders for order cards. - Updated `ClientHomeBody` to show the skeleton during loading states. - Enhanced shimmer effects in `UiShimmerListItem`, `UiShimmerStatsCard`, and other UI components for consistency. - Introduced `isDataLoaded` state in order-related BLoCs to manage loading states effectively.
This commit is contained in:
@@ -223,7 +223,9 @@ A PR is approved ONLY when ALL of these are true:
|
|||||||
- Zero CRITICAL violations
|
- Zero CRITICAL violations
|
||||||
- Zero HIGH violations
|
- Zero HIGH violations
|
||||||
- MODERATE violations have a documented plan or justification
|
- 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
|
- Test coverage ≥ 70% for business logic
|
||||||
- Design system fully compliant
|
- Design system fully compliant
|
||||||
- Architecture boundaries fully respected
|
- Architecture boundaries fully respected
|
||||||
|
|||||||
@@ -325,6 +325,8 @@
|
|||||||
"client_create_order": {
|
"client_create_order": {
|
||||||
"title": "Create Order",
|
"title": "Create Order",
|
||||||
"section_title": "ORDER TYPE",
|
"section_title": "ORDER TYPE",
|
||||||
|
"no_vendors_title": "No Vendors Available",
|
||||||
|
"no_vendors_description": "There are no staffing vendors associated with your account.",
|
||||||
"types": {
|
"types": {
|
||||||
"rapid": "RAPID",
|
"rapid": "RAPID",
|
||||||
"rapid_desc": "URGENT same-day Coverage",
|
"rapid_desc": "URGENT same-day Coverage",
|
||||||
|
|||||||
@@ -325,6 +325,8 @@
|
|||||||
"client_create_order": {
|
"client_create_order": {
|
||||||
"title": "Crear Orden",
|
"title": "Crear Orden",
|
||||||
"section_title": "TIPO DE 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": {
|
"types": {
|
||||||
"rapid": "R\u00c1PIDO",
|
"rapid": "R\u00c1PIDO",
|
||||||
"rapid_desc": "Cobertura URGENTE mismo d\u00eda",
|
"rapid_desc": "Cobertura URGENTE mismo d\u00eda",
|
||||||
|
|||||||
@@ -12,26 +12,25 @@ class UiShimmerListItem extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return const Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
vertical: UiConstants.space2,
|
vertical: UiConstants.space2,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
spacing: UiConstants.space3,
|
||||||
const UiShimmerCircle(size: UiConstants.space10),
|
children: <Widget>[
|
||||||
const SizedBox(width: UiConstants.space3),
|
UiShimmerCircle(size: UiConstants.space10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
spacing: UiConstants.space2,
|
||||||
const UiShimmerLine(width: 160),
|
children: <Widget>[
|
||||||
const SizedBox(height: UiConstants.space2),
|
UiShimmerLine(width: 160),
|
||||||
const UiShimmerLine(width: 100, height: 12),
|
UiShimmerLine(width: 100, height: 12),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: UiConstants.space3),
|
UiShimmerBox(width: 48, height: 24),
|
||||||
const UiShimmerBox(width: 48, height: 24),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -56,14 +55,14 @@ class UiShimmerStatsCard extends StatelessWidget {
|
|||||||
borderRadius: UiConstants.radiusLg,
|
borderRadius: UiConstants.radiusLg,
|
||||||
color: UiColors.cardViewBackground,
|
color: UiColors.cardViewBackground,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: const Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: <Widget>[
|
||||||
const UiShimmerCircle(size: UiConstants.space8),
|
UiShimmerCircle(size: UiConstants.space8),
|
||||||
const SizedBox(height: UiConstants.space3),
|
SizedBox(height: UiConstants.space3),
|
||||||
const UiShimmerLine(width: 80, height: 12),
|
UiShimmerLine(width: 80, height: 12),
|
||||||
const SizedBox(height: UiConstants.space2),
|
SizedBox(height: UiConstants.space2),
|
||||||
const UiShimmerLine(width: 120, height: 20),
|
UiShimmerLine(width: 120, height: 20),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -110,9 +109,9 @@ class UiShimmerList extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final gap = spacing ?? UiConstants.space3;
|
final double gap = spacing ?? UiConstants.space3;
|
||||||
return Column(
|
return Column(
|
||||||
children: List.generate(itemCount, (index) {
|
children: List<Widget>.generate(itemCount, (int index) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(bottom: index < itemCount - 1 ? gap : 0),
|
padding: EdgeInsets.only(bottom: index < itemCount - 1 ? gap : 0),
|
||||||
child: itemBuilder(index),
|
child: itemBuilder(index),
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ import '../blocs/client_home_state.dart';
|
|||||||
import 'client_home_edit_mode_body.dart';
|
import 'client_home_edit_mode_body.dart';
|
||||||
import 'client_home_error_state.dart';
|
import 'client_home_error_state.dart';
|
||||||
import 'client_home_normal_mode_body.dart';
|
import 'client_home_normal_mode_body.dart';
|
||||||
|
import 'client_home_page_skeleton.dart';
|
||||||
|
|
||||||
/// Main body widget for the client home page.
|
/// 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 {
|
class ClientHomeBody extends StatelessWidget {
|
||||||
/// Creates a [ClientHomeBody].
|
/// Creates a [ClientHomeBody].
|
||||||
const ClientHomeBody({super.key});
|
const ClientHomeBody({super.key});
|
||||||
@@ -31,6 +33,11 @@ class ClientHomeBody extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
builder: (BuildContext context, ClientHomeState state) {
|
builder: (BuildContext context, ClientHomeState state) {
|
||||||
|
return const ClientHomePageSkeleton();
|
||||||
|
if (state.status == ClientHomeStatus.initial ||
|
||||||
|
state.status == ClientHomeStatus.loading) {
|
||||||
|
return const ClientHomePageSkeleton();
|
||||||
|
}
|
||||||
if (state.status == ClientHomeStatus.error) {
|
if (state.status == ClientHomeStatus.error) {
|
||||||
return ClientHomeErrorState(state: state);
|
return ClientHomeErrorState(state: state);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,329 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Skeleton for the two side-by-side action cards.
|
||||||
|
class _ActionsSectionSkeleton extends StatelessWidget {
|
||||||
|
const _ActionsSectionSkeleton();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
const UiShimmerSectionHeader(),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(child: _ActionCardSkeleton()),
|
||||||
|
const SizedBox(width: UiConstants.space4),
|
||||||
|
Expanded(child: _ActionCardSkeleton()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Skeleton for a single action card with icon, title, and subtitle.
|
||||||
|
class _ActionCardSkeleton extends StatelessWidget {
|
||||||
|
@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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Skeleton for the horizontal reorder cards list.
|
||||||
|
class _ReorderSectionSkeleton extends StatelessWidget {
|
||||||
|
const _ReorderSectionSkeleton();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
const UiShimmerSectionHeader(),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
SizedBox(
|
||||||
|
height: 164,
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
_ReorderCardSkeleton(),
|
||||||
|
const SizedBox(width: UiConstants.space3),
|
||||||
|
_ReorderCardSkeleton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Skeleton for a single reorder card.
|
||||||
|
class _ReorderCardSkeleton extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: 260,
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Skeleton for the coverage metric cards row.
|
||||||
|
class _CoverageSectionSkeleton extends StatelessWidget {
|
||||||
|
const _CoverageSectionSkeleton();
|
||||||
|
|
||||||
|
@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()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Skeleton for a single coverage metric card.
|
||||||
|
class _MetricCardSkeleton extends StatelessWidget {
|
||||||
|
const _MetricCardSkeleton();
|
||||||
|
|
||||||
|
@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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Skeleton for the spending gradient card.
|
||||||
|
class _SpendingSectionSkeleton extends StatelessWidget {
|
||||||
|
const _SpendingSectionSkeleton();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
UiShimmerSectionHeader(),
|
||||||
|
SizedBox(height: UiConstants.space2),
|
||||||
|
_SpendingCardSkeleton(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Skeleton mimicking the spending card layout.
|
||||||
|
class _SpendingCardSkeleton extends StatelessWidget {
|
||||||
|
const _SpendingCardSkeleton();
|
||||||
|
|
||||||
|
@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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Skeleton for the live activity section.
|
||||||
|
class _LiveActivitySectionSkeleton extends StatelessWidget {
|
||||||
|
const _LiveActivitySectionSkeleton();
|
||||||
|
|
||||||
|
@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(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -149,7 +149,11 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
|||||||
? event.vendors.first
|
? event.vendors.first
|
||||||
: null;
|
: null;
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(vendors: event.vendors, selectedVendor: selectedVendor),
|
state.copyWith(
|
||||||
|
vendors: event.vendors,
|
||||||
|
selectedVendor: selectedVendor,
|
||||||
|
isDataLoaded: true,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (selectedVendor != null) {
|
if (selectedVendor != null) {
|
||||||
await _loadRolesForVendor(selectedVendor.id, emit);
|
await _loadRolesForVendor(selectedVendor.id, emit);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class OneTimeOrderState extends Equatable {
|
|||||||
this.managers = const <OneTimeOrderManagerOption>[],
|
this.managers = const <OneTimeOrderManagerOption>[],
|
||||||
this.selectedManager,
|
this.selectedManager,
|
||||||
this.isRapidDraft = false,
|
this.isRapidDraft = false,
|
||||||
|
this.isDataLoaded = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory OneTimeOrderState.initial() {
|
factory OneTimeOrderState.initial() {
|
||||||
@@ -52,6 +53,9 @@ class OneTimeOrderState extends Equatable {
|
|||||||
final OneTimeOrderManagerOption? selectedManager;
|
final OneTimeOrderManagerOption? selectedManager;
|
||||||
final bool isRapidDraft;
|
final bool isRapidDraft;
|
||||||
|
|
||||||
|
/// Whether initial data (vendors, hubs) has been fetched from the backend.
|
||||||
|
final bool isDataLoaded;
|
||||||
|
|
||||||
OneTimeOrderState copyWith({
|
OneTimeOrderState copyWith({
|
||||||
DateTime? date,
|
DateTime? date,
|
||||||
String? location,
|
String? location,
|
||||||
@@ -67,6 +71,7 @@ class OneTimeOrderState extends Equatable {
|
|||||||
List<OneTimeOrderManagerOption>? managers,
|
List<OneTimeOrderManagerOption>? managers,
|
||||||
OneTimeOrderManagerOption? selectedManager,
|
OneTimeOrderManagerOption? selectedManager,
|
||||||
bool? isRapidDraft,
|
bool? isRapidDraft,
|
||||||
|
bool? isDataLoaded,
|
||||||
}) {
|
}) {
|
||||||
return OneTimeOrderState(
|
return OneTimeOrderState(
|
||||||
date: date ?? this.date,
|
date: date ?? this.date,
|
||||||
@@ -83,6 +88,7 @@ class OneTimeOrderState extends Equatable {
|
|||||||
managers: managers ?? this.managers,
|
managers: managers ?? this.managers,
|
||||||
selectedManager: selectedManager ?? this.selectedManager,
|
selectedManager: selectedManager ?? this.selectedManager,
|
||||||
isRapidDraft: isRapidDraft ?? this.isRapidDraft,
|
isRapidDraft: isRapidDraft ?? this.isRapidDraft,
|
||||||
|
isDataLoaded: isDataLoaded ?? this.isDataLoaded,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +193,7 @@ class OneTimeOrderState extends Equatable {
|
|||||||
managers,
|
managers,
|
||||||
selectedManager,
|
selectedManager,
|
||||||
isRapidDraft,
|
isRapidDraft,
|
||||||
|
isDataLoaded,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -136,7 +136,11 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
|||||||
? event.vendors.first
|
? event.vendors.first
|
||||||
: null;
|
: null;
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(vendors: event.vendors, selectedVendor: selectedVendor),
|
state.copyWith(
|
||||||
|
vendors: event.vendors,
|
||||||
|
selectedVendor: selectedVendor,
|
||||||
|
isDataLoaded: true,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (selectedVendor != null) {
|
if (selectedVendor != null) {
|
||||||
await _loadRolesForVendor(selectedVendor.id, emit);
|
await _loadRolesForVendor(selectedVendor.id, emit);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class PermanentOrderState extends Equatable {
|
|||||||
this.roles = const <PermanentOrderRoleOption>[],
|
this.roles = const <PermanentOrderRoleOption>[],
|
||||||
this.managers = const <PermanentOrderManagerOption>[],
|
this.managers = const <PermanentOrderManagerOption>[],
|
||||||
this.selectedManager,
|
this.selectedManager,
|
||||||
|
this.isDataLoaded = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory PermanentOrderState.initial() {
|
factory PermanentOrderState.initial() {
|
||||||
@@ -68,6 +69,9 @@ class PermanentOrderState extends Equatable {
|
|||||||
final List<PermanentOrderManagerOption> managers;
|
final List<PermanentOrderManagerOption> managers;
|
||||||
final PermanentOrderManagerOption? selectedManager;
|
final PermanentOrderManagerOption? selectedManager;
|
||||||
|
|
||||||
|
/// Whether initial data (vendors, hubs) has been fetched from the backend.
|
||||||
|
final bool isDataLoaded;
|
||||||
|
|
||||||
PermanentOrderState copyWith({
|
PermanentOrderState copyWith({
|
||||||
DateTime? startDate,
|
DateTime? startDate,
|
||||||
List<String>? permanentDays,
|
List<String>? permanentDays,
|
||||||
@@ -84,6 +88,7 @@ class PermanentOrderState extends Equatable {
|
|||||||
List<PermanentOrderRoleOption>? roles,
|
List<PermanentOrderRoleOption>? roles,
|
||||||
List<PermanentOrderManagerOption>? managers,
|
List<PermanentOrderManagerOption>? managers,
|
||||||
PermanentOrderManagerOption? selectedManager,
|
PermanentOrderManagerOption? selectedManager,
|
||||||
|
bool? isDataLoaded,
|
||||||
}) {
|
}) {
|
||||||
return PermanentOrderState(
|
return PermanentOrderState(
|
||||||
startDate: startDate ?? this.startDate,
|
startDate: startDate ?? this.startDate,
|
||||||
@@ -101,6 +106,7 @@ class PermanentOrderState extends Equatable {
|
|||||||
roles: roles ?? this.roles,
|
roles: roles ?? this.roles,
|
||||||
managers: managers ?? this.managers,
|
managers: managers ?? this.managers,
|
||||||
selectedManager: selectedManager ?? this.selectedManager,
|
selectedManager: selectedManager ?? this.selectedManager,
|
||||||
|
isDataLoaded: isDataLoaded ?? this.isDataLoaded,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,6 +192,7 @@ class PermanentOrderState extends Equatable {
|
|||||||
roles,
|
roles,
|
||||||
managers,
|
managers,
|
||||||
selectedManager,
|
selectedManager,
|
||||||
|
isDataLoaded,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -149,7 +149,11 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
|||||||
? event.vendors.first
|
? event.vendors.first
|
||||||
: null;
|
: null;
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(vendors: event.vendors, selectedVendor: selectedVendor),
|
state.copyWith(
|
||||||
|
vendors: event.vendors,
|
||||||
|
selectedVendor: selectedVendor,
|
||||||
|
isDataLoaded: true,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (selectedVendor != null) {
|
if (selectedVendor != null) {
|
||||||
await _loadRolesForVendor(selectedVendor.id, emit);
|
await _loadRolesForVendor(selectedVendor.id, emit);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class RecurringOrderState extends Equatable {
|
|||||||
this.roles = const <RecurringOrderRoleOption>[],
|
this.roles = const <RecurringOrderRoleOption>[],
|
||||||
this.managers = const <RecurringOrderManagerOption>[],
|
this.managers = const <RecurringOrderManagerOption>[],
|
||||||
this.selectedManager,
|
this.selectedManager,
|
||||||
|
this.isDataLoaded = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory RecurringOrderState.initial() {
|
factory RecurringOrderState.initial() {
|
||||||
@@ -72,6 +73,9 @@ class RecurringOrderState extends Equatable {
|
|||||||
final List<RecurringOrderManagerOption> managers;
|
final List<RecurringOrderManagerOption> managers;
|
||||||
final RecurringOrderManagerOption? selectedManager;
|
final RecurringOrderManagerOption? selectedManager;
|
||||||
|
|
||||||
|
/// Whether initial data (vendors, hubs) has been fetched from the backend.
|
||||||
|
final bool isDataLoaded;
|
||||||
|
|
||||||
RecurringOrderState copyWith({
|
RecurringOrderState copyWith({
|
||||||
DateTime? startDate,
|
DateTime? startDate,
|
||||||
DateTime? endDate,
|
DateTime? endDate,
|
||||||
@@ -89,6 +93,7 @@ class RecurringOrderState extends Equatable {
|
|||||||
List<RecurringOrderRoleOption>? roles,
|
List<RecurringOrderRoleOption>? roles,
|
||||||
List<RecurringOrderManagerOption>? managers,
|
List<RecurringOrderManagerOption>? managers,
|
||||||
RecurringOrderManagerOption? selectedManager,
|
RecurringOrderManagerOption? selectedManager,
|
||||||
|
bool? isDataLoaded,
|
||||||
}) {
|
}) {
|
||||||
return RecurringOrderState(
|
return RecurringOrderState(
|
||||||
startDate: startDate ?? this.startDate,
|
startDate: startDate ?? this.startDate,
|
||||||
@@ -107,6 +112,7 @@ class RecurringOrderState extends Equatable {
|
|||||||
roles: roles ?? this.roles,
|
roles: roles ?? this.roles,
|
||||||
managers: managers ?? this.managers,
|
managers: managers ?? this.managers,
|
||||||
selectedManager: selectedManager ?? this.selectedManager,
|
selectedManager: selectedManager ?? this.selectedManager,
|
||||||
|
isDataLoaded: isDataLoaded ?? this.isDataLoaded,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,6 +220,7 @@ class RecurringOrderState extends Equatable {
|
|||||||
roles,
|
roles,
|
||||||
managers,
|
managers,
|
||||||
selectedManager,
|
selectedManager,
|
||||||
|
isDataLoaded,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ class OneTimeOrderPage extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return OneTimeOrderView(
|
return OneTimeOrderView(
|
||||||
|
isDataLoaded: state.isDataLoaded,
|
||||||
status: _mapStatus(state.status),
|
status: _mapStatus(state.status),
|
||||||
errorMessage: state.errorMessage,
|
errorMessage: state.errorMessage,
|
||||||
eventName: state.eventName,
|
eventName: state.eventName,
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ class PermanentOrderPage extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return PermanentOrderView(
|
return PermanentOrderView(
|
||||||
|
isDataLoaded: state.isDataLoaded,
|
||||||
status: _mapStatus(state.status),
|
status: _mapStatus(state.status),
|
||||||
errorMessage: state.errorMessage,
|
errorMessage: state.errorMessage,
|
||||||
eventName: state.eventName,
|
eventName: state.eventName,
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class RecurringOrderPage extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return RecurringOrderView(
|
return RecurringOrderView(
|
||||||
|
isDataLoaded: state.isDataLoaded,
|
||||||
status: _mapStatus(state.status),
|
status: _mapStatus(state.status),
|
||||||
errorMessage: state.errorMessage,
|
errorMessage: state.errorMessage,
|
||||||
eventName: state.eventName,
|
eventName: state.eventName,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export 'src/presentation/widgets/order_ui_models.dart';
|
|||||||
|
|
||||||
// Shared Widgets
|
// Shared Widgets
|
||||||
export 'src/presentation/widgets/order_bottom_action_button.dart';
|
export 'src/presentation/widgets/order_bottom_action_button.dart';
|
||||||
|
export 'src/presentation/widgets/order_form_skeleton.dart';
|
||||||
|
|
||||||
// One Time Order Widgets
|
// One Time Order Widgets
|
||||||
export 'src/presentation/widgets/one_time_order/one_time_order_date_picker.dart';
|
export 'src/presentation/widgets/one_time_order/one_time_order_date_picker.dart';
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
import '../order_bottom_action_button.dart';
|
import '../order_bottom_action_button.dart';
|
||||||
|
import '../order_form_skeleton.dart';
|
||||||
import '../order_ui_models.dart';
|
import '../order_ui_models.dart';
|
||||||
import 'one_time_order_form.dart';
|
import 'one_time_order_form.dart';
|
||||||
import 'one_time_order_success_view.dart';
|
import 'one_time_order_success_view.dart';
|
||||||
@@ -37,6 +38,7 @@ class OneTimeOrderView extends StatelessWidget {
|
|||||||
required this.onBack,
|
required this.onBack,
|
||||||
this.title,
|
this.title,
|
||||||
this.subtitle,
|
this.subtitle,
|
||||||
|
this.isDataLoaded = true,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -56,6 +58,9 @@ class OneTimeOrderView extends StatelessWidget {
|
|||||||
final String? title;
|
final String? title;
|
||||||
final String? subtitle;
|
final String? subtitle;
|
||||||
|
|
||||||
|
/// Whether initial data (vendors, hubs) has been fetched from the backend.
|
||||||
|
final bool isDataLoaded;
|
||||||
|
|
||||||
final ValueChanged<String> onEventNameChanged;
|
final ValueChanged<String> onEventNameChanged;
|
||||||
final ValueChanged<Vendor> onVendorChanged;
|
final ValueChanged<Vendor> onVendorChanged;
|
||||||
final ValueChanged<DateTime> onDateChanged;
|
final ValueChanged<DateTime> onDateChanged;
|
||||||
@@ -81,7 +86,12 @@ class OneTimeOrderView extends StatelessWidget {
|
|||||||
context,
|
context,
|
||||||
message: translateErrorKey(errorMessage!),
|
message: translateErrorKey(errorMessage!),
|
||||||
type: UiSnackbarType.error,
|
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,
|
BuildContext context,
|
||||||
TranslationsClientCreateOrderOneTimeEn labels,
|
TranslationsClientCreateOrderOneTimeEn labels,
|
||||||
) {
|
) {
|
||||||
|
if (!isDataLoaded) {
|
||||||
|
return const OrderFormSkeleton();
|
||||||
|
}
|
||||||
|
|
||||||
if (vendors.isEmpty && status != OrderFormStatus.loading) {
|
if (vendors.isEmpty && status != OrderFormStatus.loading) {
|
||||||
return Column(
|
return Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@@ -126,12 +140,12 @@ class OneTimeOrderView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space4),
|
const SizedBox(height: UiConstants.space4),
|
||||||
Text(
|
Text(
|
||||||
'No Vendors Available',
|
t.client_create_order.no_vendors_title,
|
||||||
style: UiTypography.headline3m.textPrimary,
|
style: UiTypography.headline3m.textPrimary,
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space2),
|
const SizedBox(height: UiConstants.space2),
|
||||||
Text(
|
Text(
|
||||||
'There are no staffing vendors associated with your account.',
|
t.client_create_order.no_vendors_description,
|
||||||
style: UiTypography.body2r.textSecondary,
|
style: UiTypography.body2r.textSecondary,
|
||||||
textAlign: TextAlign.center,
|
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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:krow_domain/krow_domain.dart' show Vendor;
|
import 'package:krow_domain/krow_domain.dart' show Vendor;
|
||||||
|
|
||||||
import '../order_bottom_action_button.dart';
|
import '../order_bottom_action_button.dart';
|
||||||
|
import '../order_form_skeleton.dart';
|
||||||
import '../order_ui_models.dart';
|
import '../order_ui_models.dart';
|
||||||
import 'permanent_order_form.dart';
|
import 'permanent_order_form.dart';
|
||||||
import 'permanent_order_success_view.dart';
|
import 'permanent_order_success_view.dart';
|
||||||
@@ -37,9 +38,12 @@ class PermanentOrderView extends StatelessWidget {
|
|||||||
required this.onSubmit,
|
required this.onSubmit,
|
||||||
required this.onDone,
|
required this.onDone,
|
||||||
required this.onBack,
|
required this.onBack,
|
||||||
|
this.isDataLoaded = true,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Whether initial data (vendors, hubs) has been fetched from the backend.
|
||||||
|
final bool isDataLoaded;
|
||||||
final OrderFormStatus status;
|
final OrderFormStatus status;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
final String eventName;
|
final String eventName;
|
||||||
@@ -82,7 +86,12 @@ class PermanentOrderView extends StatelessWidget {
|
|||||||
context,
|
context,
|
||||||
message: translateErrorKey(errorMessage!),
|
message: translateErrorKey(errorMessage!),
|
||||||
type: UiSnackbarType.error,
|
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,
|
TranslationsClientCreateOrderPermanentEn labels,
|
||||||
TranslationsClientCreateOrderOneTimeEn oneTimeLabels,
|
TranslationsClientCreateOrderOneTimeEn oneTimeLabels,
|
||||||
) {
|
) {
|
||||||
|
if (!isDataLoaded) {
|
||||||
|
return const OrderFormSkeleton();
|
||||||
|
}
|
||||||
|
|
||||||
if (vendors.isEmpty && status != OrderFormStatus.loading) {
|
if (vendors.isEmpty && status != OrderFormStatus.loading) {
|
||||||
return Column(
|
return Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@@ -128,12 +141,12 @@ class PermanentOrderView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space4),
|
const SizedBox(height: UiConstants.space4),
|
||||||
Text(
|
Text(
|
||||||
'No Vendors Available',
|
t.client_create_order.no_vendors_title,
|
||||||
style: UiTypography.headline3m.textPrimary,
|
style: UiTypography.headline3m.textPrimary,
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space2),
|
const SizedBox(height: UiConstants.space2),
|
||||||
Text(
|
Text(
|
||||||
'There are no staffing vendors associated with your account.',
|
t.client_create_order.no_vendors_description,
|
||||||
style: UiTypography.body2r.textSecondary,
|
style: UiTypography.body2r.textSecondary,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:krow_domain/krow_domain.dart' show Vendor;
|
import 'package:krow_domain/krow_domain.dart' show Vendor;
|
||||||
|
|
||||||
import '../order_bottom_action_button.dart';
|
import '../order_bottom_action_button.dart';
|
||||||
|
import '../order_form_skeleton.dart';
|
||||||
import '../order_ui_models.dart';
|
import '../order_ui_models.dart';
|
||||||
import 'recurring_order_form.dart';
|
import 'recurring_order_form.dart';
|
||||||
import 'recurring_order_success_view.dart';
|
import 'recurring_order_success_view.dart';
|
||||||
@@ -39,9 +40,12 @@ class RecurringOrderView extends StatelessWidget {
|
|||||||
required this.onSubmit,
|
required this.onSubmit,
|
||||||
required this.onDone,
|
required this.onDone,
|
||||||
required this.onBack,
|
required this.onBack,
|
||||||
|
this.isDataLoaded = true,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Whether initial data (vendors, hubs) has been fetched from the backend.
|
||||||
|
final bool isDataLoaded;
|
||||||
final OrderFormStatus status;
|
final OrderFormStatus status;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
final String eventName;
|
final String eventName;
|
||||||
@@ -89,7 +93,12 @@ class RecurringOrderView extends StatelessWidget {
|
|||||||
context,
|
context,
|
||||||
message: message,
|
message: message,
|
||||||
type: UiSnackbarType.error,
|
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,
|
TranslationsClientCreateOrderRecurringEn labels,
|
||||||
TranslationsClientCreateOrderOneTimeEn oneTimeLabels,
|
TranslationsClientCreateOrderOneTimeEn oneTimeLabels,
|
||||||
) {
|
) {
|
||||||
|
if (!isDataLoaded) {
|
||||||
|
return const OrderFormSkeleton();
|
||||||
|
}
|
||||||
|
|
||||||
if (vendors.isEmpty && status != OrderFormStatus.loading) {
|
if (vendors.isEmpty && status != OrderFormStatus.loading) {
|
||||||
return Column(
|
return Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@@ -135,12 +148,12 @@ class RecurringOrderView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space4),
|
const SizedBox(height: UiConstants.space4),
|
||||||
Text(
|
Text(
|
||||||
'No Vendors Available',
|
t.client_create_order.no_vendors_title,
|
||||||
style: UiTypography.headline3m.textPrimary,
|
style: UiTypography.headline3m.textPrimary,
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space2),
|
const SizedBox(height: UiConstants.space2),
|
||||||
Text(
|
Text(
|
||||||
'There are no staffing vendors associated with your account.',
|
t.client_create_order.no_vendors_description,
|
||||||
style: UiTypography.body2r.textSecondary,
|
style: UiTypography.body2r.textSecondary,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import '../widgets/view_orders_header.dart';
|
|||||||
import '../widgets/view_orders_empty_state.dart';
|
import '../widgets/view_orders_empty_state.dart';
|
||||||
import '../widgets/view_orders_error_state.dart';
|
import '../widgets/view_orders_error_state.dart';
|
||||||
import '../widgets/view_orders_list.dart';
|
import '../widgets/view_orders_list.dart';
|
||||||
|
import '../widgets/view_orders_page_skeleton.dart';
|
||||||
|
|
||||||
/// The main page for viewing client orders.
|
/// The main page for viewing client orders.
|
||||||
///
|
///
|
||||||
@@ -101,20 +102,26 @@ class _ViewOrdersViewState extends State<ViewOrdersView> {
|
|||||||
|
|
||||||
// Content List
|
// Content List
|
||||||
Expanded(
|
Expanded(
|
||||||
child: state.status == ViewOrdersStatus.failure
|
child: switch (state.status) {
|
||||||
? ViewOrdersErrorState(
|
ViewOrdersStatus.loading ||
|
||||||
|
ViewOrdersStatus.initial =>
|
||||||
|
const ViewOrdersPageSkeleton(),
|
||||||
|
ViewOrdersStatus.failure => ViewOrdersErrorState(
|
||||||
errorMessage: state.errorMessage,
|
errorMessage: state.errorMessage,
|
||||||
selectedDate: state.selectedDate,
|
selectedDate: state.selectedDate,
|
||||||
onRetry: () => BlocProvider.of<ViewOrdersCubit>(
|
onRetry: () => BlocProvider.of<ViewOrdersCubit>(
|
||||||
context,
|
context,
|
||||||
).jumpToDate(state.selectedDate ?? DateTime.now()),
|
).jumpToDate(state.selectedDate ?? DateTime.now()),
|
||||||
|
),
|
||||||
|
ViewOrdersStatus.success => filteredOrders.isEmpty
|
||||||
|
? ViewOrdersEmptyState(
|
||||||
|
selectedDate: state.selectedDate,
|
||||||
)
|
)
|
||||||
: filteredOrders.isEmpty
|
|
||||||
? ViewOrdersEmptyState(selectedDate: state.selectedDate)
|
|
||||||
: ViewOrdersList(
|
: ViewOrdersList(
|
||||||
orders: filteredOrders,
|
orders: filteredOrders,
|
||||||
filterTab: state.filterTab,
|
filterTab: state.filterTab,
|
||||||
),
|
),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.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(),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shimmer placeholder for the section header row.
|
||||||
|
class _SectionHeaderSkeleton extends StatelessWidget {
|
||||||
|
const _SectionHeaderSkeleton();
|
||||||
|
|
||||||
|
@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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shimmer placeholder for a single order card.
|
||||||
|
class _OrderCardSkeleton extends StatelessWidget {
|
||||||
|
const _OrderCardSkeleton();
|
||||||
|
|
||||||
|
@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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shimmer placeholder for a single stat item (icon + value + label).
|
||||||
|
class _StatItemSkeleton extends StatelessWidget {
|
||||||
|
const _StatItemSkeleton();
|
||||||
|
|
||||||
|
@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),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shimmer placeholder for the vertical stat divider.
|
||||||
|
class _StatDividerSkeleton extends StatelessWidget {
|
||||||
|
const _StatDividerSkeleton();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const UiShimmerBox(
|
||||||
|
width: 1,
|
||||||
|
height: 24,
|
||||||
|
borderRadius: BorderRadius.zero,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user