Merge branch 'dev' into feature/session-persistence-new

This commit is contained in:
2026-03-11 10:56:48 +05:30
committed by GitHub
353 changed files with 29419 additions and 3689 deletions

View File

@@ -11,6 +11,7 @@ import '../blocs/certificates/certificates_state.dart';
import '../widgets/add_certificate_card.dart';
import '../widgets/certificate_card.dart';
import '../widgets/certificates_header.dart';
import '../widgets/certificates_skeleton/certificates_skeleton.dart';
/// Page for viewing and managing staff certificates.
///
@@ -28,9 +29,7 @@ class CertificatesPage extends StatelessWidget {
builder: (BuildContext context, CertificatesState state) {
if (state.status == CertificatesStatus.loading ||
state.status == CertificatesStatus.initial) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
return const Scaffold(body: CertificatesSkeleton());
}
if (state.status == CertificatesStatus.failure) {

View File

@@ -0,0 +1,38 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer placeholder for a single certificate card.
class CertificateCardSkeleton extends StatelessWidget {
/// Creates a [CertificateCardSkeleton].
const CertificateCardSkeleton({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(UiConstants.space4),
margin: const EdgeInsets.only(bottom: UiConstants.space3),
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border),
),
child: const Row(
children: <Widget>[
UiShimmerCircle(size: 40),
SizedBox(width: UiConstants.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UiShimmerLine(width: 140, height: 14),
SizedBox(height: UiConstants.space2),
UiShimmerLine(width: 100, height: 12),
],
),
),
UiShimmerBox(width: 60, height: 28),
],
),
);
}
}

View File

@@ -0,0 +1,37 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer placeholder for the certificates progress header.
class CertificatesHeaderSkeleton extends StatelessWidget {
/// Creates a [CertificatesHeaderSkeleton].
const CertificatesHeaderSkeleton({super.key});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(UiConstants.space5),
decoration: const BoxDecoration(color: UiColors.primary),
child: SafeArea(
bottom: false,
child: Column(
children: <Widget>[
const SizedBox(height: UiConstants.space4),
const UiShimmerCircle(size: 64),
const SizedBox(height: UiConstants.space3),
UiShimmerLine(
width: 120,
height: 14,
),
const SizedBox(height: UiConstants.space2),
UiShimmerLine(
width: 80,
height: 12,
),
const SizedBox(height: UiConstants.space6),
],
),
),
);
}
}

View File

@@ -0,0 +1,38 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'certificate_card_skeleton.dart';
import 'certificates_header_skeleton.dart';
/// Full-page shimmer skeleton shown while certificates are loading.
class CertificatesSkeleton extends StatelessWidget {
/// Creates a [CertificatesSkeleton].
const CertificatesSkeleton({super.key});
@override
Widget build(BuildContext context) {
return UiShimmer(
child: SingleChildScrollView(
child: Column(
children: <Widget>[
const CertificatesHeaderSkeleton(),
Transform.translate(
offset: const Offset(0, -UiConstants.space12),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
),
child: UiShimmerList(
itemCount: 4,
spacing: UiConstants.space3,
itemBuilder: (int index) =>
const CertificateCardSkeleton(),
),
),
),
],
),
),
);
}
}

View File

@@ -10,6 +10,7 @@ import '../blocs/documents/documents_cubit.dart';
import '../blocs/documents/documents_state.dart';
import '../widgets/document_card.dart';
import '../widgets/documents_progress_card.dart';
import '../widgets/documents_skeleton/documents_skeleton.dart';
class DocumentsPage extends StatelessWidget {
const DocumentsPage({super.key});
@@ -28,11 +29,7 @@ class DocumentsPage extends StatelessWidget {
child: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (BuildContext context, DocumentsState state) {
if (state.status == DocumentsStatus.loading) {
return const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(UiColors.primary),
),
);
return const DocumentsSkeleton();
}
if (state.status == DocumentsStatus.failure) {
return Center(

View File

@@ -0,0 +1,37 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer placeholder for a single document card row.
class DocumentCardSkeleton extends StatelessWidget {
/// Creates a [DocumentCardSkeleton].
const DocumentCardSkeleton({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
border: Border.all(color: UiColors.border),
borderRadius: UiConstants.radiusLg,
color: UiColors.cardViewBackground,
),
child: const Row(
children: <Widget>[
UiShimmerCircle(size: 40),
SizedBox(width: UiConstants.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UiShimmerLine(width: 160, height: 14),
SizedBox(height: UiConstants.space2),
UiShimmerLine(width: 100, height: 12),
],
),
),
UiShimmerBox(width: 24, height: 24),
],
),
);
}
}

View File

@@ -0,0 +1,30 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer placeholder for the documents progress card.
class DocumentsProgressSkeleton extends StatelessWidget {
/// Creates a [DocumentsProgressSkeleton].
const DocumentsProgressSkeleton({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
border: Border.all(color: UiColors.border),
borderRadius: UiConstants.radiusLg,
color: UiColors.cardViewBackground,
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UiShimmerLine(width: 140, height: 14),
SizedBox(height: UiConstants.space3),
UiShimmerBox(width: double.infinity, height: 8),
SizedBox(height: UiConstants.space2),
UiShimmerLine(width: 80, height: 12),
],
),
);
}
}

View File

@@ -0,0 +1,32 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'document_card_skeleton.dart';
import 'documents_progress_skeleton.dart';
/// Full-page shimmer skeleton shown while documents are loading.
class DocumentsSkeleton extends StatelessWidget {
/// Creates a [DocumentsSkeleton].
const DocumentsSkeleton({super.key});
@override
Widget build(BuildContext context) {
return UiShimmer(
child: ListView(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
vertical: UiConstants.space6,
),
children: <Widget>[
const DocumentsProgressSkeleton(),
const SizedBox(height: UiConstants.space4),
UiShimmerList(
itemCount: 5,
spacing: UiConstants.space3,
itemBuilder: (int index) => const DocumentCardSkeleton(),
),
],
),
);
}
}

View File

@@ -8,6 +8,7 @@ import 'package:krow_domain/krow_domain.dart';
import '../blocs/tax_forms/tax_forms_cubit.dart';
import '../blocs/tax_forms/tax_forms_state.dart';
import '../widgets/tax_forms_page/index.dart';
import '../widgets/tax_forms_skeleton/tax_forms_skeleton.dart';
class TaxFormsPage extends StatelessWidget {
const TaxFormsPage({super.key});
@@ -31,7 +32,7 @@ class TaxFormsPage extends StatelessWidget {
child: BlocBuilder<TaxFormsCubit, TaxFormsState>(
builder: (BuildContext context, TaxFormsState state) {
if (state.status == TaxFormsStatus.loading) {
return const Center(child: CircularProgressIndicator());
return const TaxFormsSkeleton();
}
if (state.status == TaxFormsStatus.failure) {

View File

@@ -0,0 +1,37 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer placeholder for a single tax form card.
class TaxFormCardSkeleton extends StatelessWidget {
/// Creates a [TaxFormCardSkeleton].
const TaxFormCardSkeleton({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
border: Border.all(color: UiColors.border),
borderRadius: UiConstants.radiusLg,
color: UiColors.cardViewBackground,
),
child: const Row(
children: <Widget>[
UiShimmerCircle(size: 40),
SizedBox(width: UiConstants.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UiShimmerLine(width: 120, height: 14),
SizedBox(height: UiConstants.space2),
UiShimmerLine(width: 80, height: 12),
],
),
),
UiShimmerBox(width: 60, height: 24),
],
),
);
}
}

View File

@@ -0,0 +1,55 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'tax_form_card_skeleton.dart';
/// Full-page shimmer skeleton shown while tax forms are loading.
class TaxFormsSkeleton extends StatelessWidget {
/// Creates a [TaxFormsSkeleton].
const TaxFormsSkeleton({super.key});
@override
Widget build(BuildContext context) {
return UiShimmer(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
vertical: UiConstants.space6,
),
child: Column(
spacing: UiConstants.space4,
children: <Widget>[
// Info card placeholder
Container(
width: double.infinity,
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
border: Border.all(color: UiColors.border),
borderRadius: UiConstants.radiusLg,
color: UiColors.cardViewBackground,
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UiShimmerLine(width: 180, height: 14),
SizedBox(height: UiConstants.space2),
UiShimmerLine(height: 12),
SizedBox(height: UiConstants.space1),
UiShimmerLine(width: 200, height: 12),
],
),
),
// Progress overview placeholder
const UiShimmerStatsCard(),
// Form card placeholders
UiShimmerList(
itemCount: 3,
spacing: UiConstants.space2,
itemBuilder: (int index) => const TaxFormCardSkeleton(),
),
],
),
),
);
}
}

View File

@@ -10,6 +10,7 @@ import '../blocs/bank_account_cubit.dart';
import '../blocs/bank_account_state.dart';
import '../widgets/account_card.dart';
import '../widgets/add_account_form.dart';
import '../widgets/bank_account_skeleton/bank_account_skeleton.dart';
import '../widgets/security_notice.dart';
class BankAccountPage extends StatelessWidget {
@@ -49,7 +50,7 @@ class BankAccountPage extends StatelessWidget {
builder: (BuildContext context, BankAccountState state) {
if (state.status == BankAccountStatus.loading &&
state.accounts.isEmpty) {
return const Center(child: CircularProgressIndicator());
return const BankAccountSkeleton();
}
if (state.status == BankAccountStatus.error) {

View File

@@ -0,0 +1,37 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer placeholder for a single bank account card.
class AccountCardSkeleton extends StatelessWidget {
/// Creates an [AccountCardSkeleton].
const AccountCardSkeleton({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
border: Border.all(color: UiColors.border),
borderRadius: UiConstants.radiusLg,
color: UiColors.cardViewBackground,
),
child: const Row(
children: <Widget>[
UiShimmerCircle(size: 40),
SizedBox(width: UiConstants.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UiShimmerLine(width: 140, height: 14),
SizedBox(height: UiConstants.space2),
UiShimmerLine(width: 100, height: 12),
],
),
),
UiShimmerBox(width: 48, height: 24),
],
),
);
}
}

View File

@@ -0,0 +1,32 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'account_card_skeleton.dart';
import 'security_notice_skeleton.dart';
/// Full-page shimmer skeleton shown while bank accounts are loading.
class BankAccountSkeleton extends StatelessWidget {
/// Creates a [BankAccountSkeleton].
const BankAccountSkeleton({super.key});
@override
Widget build(BuildContext context) {
return UiShimmer(
child: SingleChildScrollView(
padding: const EdgeInsets.all(UiConstants.space4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const SecurityNoticeSkeleton(),
const SizedBox(height: UiConstants.space4),
UiShimmerList(
itemCount: 2,
spacing: UiConstants.space3,
itemBuilder: (int index) => const AccountCardSkeleton(),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,36 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer placeholder for the security notice banner.
class SecurityNoticeSkeleton extends StatelessWidget {
/// Creates a [SecurityNoticeSkeleton].
const SecurityNoticeSkeleton({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
border: Border.all(color: UiColors.border),
borderRadius: UiConstants.radiusLg,
color: UiColors.cardViewBackground,
),
child: const Row(
children: <Widget>[
UiShimmerCircle(size: 24),
SizedBox(width: UiConstants.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UiShimmerLine(width: 160, height: 14),
SizedBox(height: UiConstants.space2),
UiShimmerLine(height: 12),
],
),
),
],
),
);
}
}

View File

@@ -8,6 +8,7 @@ import 'package:krow_core/core.dart';
import '../blocs/time_card_bloc.dart';
import '../widgets/month_selector.dart';
import '../widgets/shift_history_list.dart';
import '../widgets/time_card_skeleton/time_card_skeleton.dart';
import '../widgets/time_card_summary.dart';
/// The main page for displaying the staff time card.
@@ -50,7 +51,7 @@ class _TimeCardPageState extends State<TimeCardPage> {
},
builder: (BuildContext context, TimeCardState state) {
if (state is TimeCardLoading) {
return const Center(child: CircularProgressIndicator());
return const TimeCardSkeleton();
} else if (state is TimeCardError) {
return Center(
child: Padding(

View File

@@ -0,0 +1,20 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer placeholder for the month selector row.
class MonthSelectorSkeleton extends StatelessWidget {
/// Creates a [MonthSelectorSkeleton].
const MonthSelectorSkeleton({super.key});
@override
Widget build(BuildContext context) {
return const Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
UiShimmerCircle(size: 32),
UiShimmerLine(width: 120, height: 16),
UiShimmerCircle(size: 32),
],
);
}
}

View File

@@ -0,0 +1,42 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer placeholder for a single shift history row.
class ShiftHistorySkeleton extends StatelessWidget {
/// Creates a [ShiftHistorySkeleton].
const ShiftHistorySkeleton({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
border: Border.all(color: UiColors.border),
borderRadius: UiConstants.radiusLg,
color: UiColors.cardViewBackground,
),
child: const Row(
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UiShimmerLine(width: 140, height: 14),
SizedBox(height: UiConstants.space2),
UiShimmerLine(width: 100, height: 12),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
UiShimmerLine(width: 60, height: 14),
SizedBox(height: UiConstants.space2),
UiShimmerLine(width: 40, height: 12),
],
),
],
),
);
}
}

View File

@@ -0,0 +1,37 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'month_selector_skeleton.dart';
import 'shift_history_skeleton.dart';
import 'time_card_summary_skeleton.dart';
/// Full-page shimmer skeleton shown while time card data is loading.
class TimeCardSkeleton extends StatelessWidget {
/// Creates a [TimeCardSkeleton].
const TimeCardSkeleton({super.key});
@override
Widget build(BuildContext context) {
return UiShimmer(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
vertical: UiConstants.space6,
),
child: Column(
children: <Widget>[
const MonthSelectorSkeleton(),
const SizedBox(height: UiConstants.space6),
const TimeCardSummarySkeleton(),
const SizedBox(height: UiConstants.space6),
UiShimmerList(
itemCount: 5,
spacing: UiConstants.space3,
itemBuilder: (int index) => const ShiftHistorySkeleton(),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,19 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer placeholder for the time card summary (hours + earnings).
class TimeCardSummarySkeleton extends StatelessWidget {
/// Creates a [TimeCardSummarySkeleton].
const TimeCardSummarySkeleton({super.key});
@override
Widget build(BuildContext context) {
return const Row(
children: <Widget>[
Expanded(child: UiShimmerStatsCard()),
SizedBox(width: UiConstants.space3),
Expanded(child: UiShimmerStatsCard()),
],
);
}
}

View File

@@ -8,13 +8,24 @@ import 'package:krow_domain/krow_domain.dart';
import 'package:staff_attire/src/presentation/blocs/attire/attire_cubit.dart';
import 'package:staff_attire/src/presentation/blocs/attire/attire_state.dart';
import '../widgets/attire_filter_chips.dart';
import '../widgets/attire_empty_section.dart';
import '../widgets/attire_info_card.dart';
import '../widgets/attire_item_card.dart';
import '../widgets/attire_section_header.dart';
import '../widgets/attire_section_tab.dart';
import '../widgets/attire_skeleton/attire_skeleton.dart';
class AttirePage extends StatelessWidget {
class AttirePage extends StatefulWidget {
const AttirePage({super.key});
@override
State<AttirePage> createState() => _AttirePageState();
}
class _AttirePageState extends State<AttirePage> {
bool _showRequired = true;
bool _showNonEssential = true;
@override
Widget build(BuildContext context) {
final AttireCubit cubit = Modular.get<AttireCubit>();
@@ -39,10 +50,15 @@ class AttirePage extends StatelessWidget {
},
builder: (BuildContext context, AttireState state) {
if (state.status == AttireStatus.loading && state.options.isEmpty) {
return const Center(child: CircularProgressIndicator());
return const AttireSkeleton();
}
final List<AttireItem> filteredOptions = state.filteredOptions;
final List<AttireItem> requiredItems = state.options
.where((AttireItem item) => item.isMandatory)
.toList();
final List<AttireItem> nonEssentialItems = state.options
.where((AttireItem item) => !item.isMandatory)
.toList();
return Column(
children: <Widget>[
@@ -55,55 +71,110 @@ class AttirePage extends StatelessWidget {
const AttireInfoCard(),
const SizedBox(height: UiConstants.space6),
// Filter Chips
AttireFilterChips(
selectedFilter: state.filter,
onFilterChanged: cubit.updateFilter,
// Section toggle chips
Row(
children: <Widget>[
AttireSectionTab(
label: 'Required',
isSelected: _showRequired,
onTap: () => setState(
() => _showRequired = !_showRequired,
),
),
const SizedBox(width: UiConstants.space3),
AttireSectionTab(
label: 'Non-Essential',
isSelected: _showNonEssential,
onTap: () => setState(
() => _showNonEssential = !_showNonEssential,
),
),
],
),
const SizedBox(height: UiConstants.space6),
// Item List
if (filteredOptions.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(
vertical: UiConstants.space10,
),
child: Center(
child: Column(
children: <Widget>[
const Icon(
UiIcons.shirt,
size: 48,
color: UiColors.iconInactive,
),
const SizedBox(height: UiConstants.space4),
Text(
context.t.staff_profile_attire.capture.no_items_filter,
style: UiTypography.body1m.textSecondary,
),
],
),
// Required section
if (_showRequired) ...<Widget>[
AttireSectionHeader(
title: 'Required',
count: requiredItems.length,
),
const SizedBox(height: UiConstants.space3),
if (requiredItems.isEmpty)
AttireEmptySection(
message: context
.t
.staff_profile_attire
.capture
.no_items_filter,
)
else
...requiredItems.map((AttireItem item) {
return Padding(
padding: const EdgeInsets.only(
bottom: UiConstants.space3,
),
child: AttireItemCard(
item: item,
isUploading: false,
uploadedPhotoUrl: state.photoUrls[item.id],
onTap: () {
Modular.to.toAttireCapture(
item: item,
initialPhotoUrl: state.photoUrls[item.id],
);
},
),
);
}),
],
// Divider between sections
if (_showRequired && _showNonEssential)
const Padding(
padding: EdgeInsets.symmetric(
vertical: UiConstants.space8,
),
child: Divider(),
)
else
...filteredOptions.map((AttireItem item) {
return Padding(
padding: const EdgeInsets.only(
bottom: UiConstants.space3,
),
child: AttireItemCard(
item: item,
isUploading: false,
uploadedPhotoUrl: state.photoUrls[item.id],
onTap: () {
Modular.to.toAttireCapture(
item: item,
initialPhotoUrl: state.photoUrls[item.id],
);
},
),
);
}),
const SizedBox(height: UiConstants.space6),
// Non-Essential section
if (_showNonEssential) ...<Widget>[
AttireSectionHeader(
title: 'Non-Essential',
count: nonEssentialItems.length,
),
const SizedBox(height: UiConstants.space3),
if (nonEssentialItems.isEmpty)
AttireEmptySection(
message: context
.t
.staff_profile_attire
.capture
.no_items_filter,
)
else
...nonEssentialItems.map((AttireItem item) {
return Padding(
padding: const EdgeInsets.only(
bottom: UiConstants.space3,
),
child: AttireItemCard(
item: item,
isUploading: false,
uploadedPhotoUrl: state.photoUrls[item.id],
onTap: () {
Modular.to.toAttireCapture(
item: item,
initialPhotoUrl: state.photoUrls[item.id],
);
},
),
);
}),
],
const SizedBox(height: UiConstants.space20),
],
),
@@ -117,3 +188,4 @@ class AttirePage extends StatelessWidget {
);
}
}

View File

@@ -0,0 +1,24 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
class AttireEmptySection extends StatelessWidget {
const AttireEmptySection({super.key, required this.message});
final String message;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: UiConstants.space6),
child: Center(
child: Column(
children: <Widget>[
const Icon(UiIcons.shirt, size: 48, color: UiColors.iconInactive),
const SizedBox(height: UiConstants.space4),
Text(message, style: UiTypography.body1m.textSecondary),
],
),
),
);
}
}

View File

@@ -0,0 +1,24 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
class AttireSectionHeader extends StatelessWidget {
const AttireSectionHeader({
super.key,
required this.title,
required this.count,
});
final String title;
final int count;
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Text(title, style: UiTypography.headline4b),
const SizedBox(width: UiConstants.space2),
Text('($count)', style: UiTypography.body1m.textSecondary),
],
);
}
}

View File

@@ -0,0 +1,41 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
class AttireSectionTab extends StatelessWidget {
const AttireSectionTab({
super.key,
required this.label,
required this.isSelected,
required this.onTap,
});
final String label;
final bool isSelected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space4,
vertical: UiConstants.space2,
),
decoration: BoxDecoration(
color: isSelected ? UiColors.primary : UiColors.white,
borderRadius: UiConstants.radiusFull,
border: Border.all(
color: isSelected ? UiColors.primary : UiColors.border,
),
),
child: Text(
label,
style: isSelected
? UiTypography.footnote2m.white
: UiTypography.footnote2m.textSecondary,
),
),
);
}
}

View File

@@ -0,0 +1,37 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer placeholder for a single attire item card.
class AttireItemSkeleton extends StatelessWidget {
/// Creates an [AttireItemSkeleton].
const AttireItemSkeleton({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
border: Border.all(color: UiColors.border),
borderRadius: UiConstants.radiusLg,
color: UiColors.cardViewBackground,
),
child: const Row(
children: <Widget>[
UiShimmerBox(width: 56, height: 56),
SizedBox(width: UiConstants.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UiShimmerLine(width: 120, height: 14),
SizedBox(height: UiConstants.space2),
UiShimmerLine(width: 80, height: 12),
],
),
),
UiShimmerBox(width: 60, height: 24),
],
),
);
}
}

View File

@@ -0,0 +1,63 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'attire_item_skeleton.dart';
/// Full-page shimmer skeleton shown while attire items are loading.
class AttireSkeleton extends StatelessWidget {
/// Creates an [AttireSkeleton].
const AttireSkeleton({super.key});
@override
Widget build(BuildContext context) {
return UiShimmer(
child: SingleChildScrollView(
padding: const EdgeInsets.all(UiConstants.space5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// Info card placeholder
Container(
width: double.infinity,
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
border: Border.all(color: UiColors.border),
borderRadius: UiConstants.radiusLg,
color: UiColors.cardViewBackground,
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UiShimmerLine(width: 160, height: 14),
SizedBox(height: UiConstants.space2),
UiShimmerLine(height: 12),
SizedBox(height: UiConstants.space1),
UiShimmerLine(width: 200, height: 12),
],
),
),
const SizedBox(height: UiConstants.space6),
// Section toggle chips placeholder
const Row(
children: <Widget>[
UiShimmerBox(width: 80, height: 32),
SizedBox(width: UiConstants.space3),
UiShimmerBox(width: 100, height: 32),
],
),
const SizedBox(height: UiConstants.space6),
// Section header placeholder
const UiShimmerSectionHeader(),
const SizedBox(height: UiConstants.space3),
// Attire item cards
UiShimmerList(
itemCount: 4,
spacing: UiConstants.space3,
itemBuilder: (int index) => const AttireItemSkeleton(),
),
],
),
),
);
}
}

View File

@@ -9,6 +9,7 @@ import '../widgets/emergency_contact_add_button.dart';
import '../widgets/emergency_contact_form_item.dart';
import '../widgets/emergency_contact_info_banner.dart';
import '../widgets/emergency_contact_save_button.dart';
import '../widgets/emergency_contact_skeleton/emergency_contact_skeleton.dart';
/// The Staff Emergency Contact screen.
///
@@ -43,7 +44,7 @@ class EmergencyContactScreen extends StatelessWidget {
},
builder: (context, state) {
if (state.status == EmergencyContactStatus.loading) {
return const Center(child: CircularProgressIndicator());
return const EmergencyContactSkeleton();
}
return Column(
children: [

View File

@@ -7,7 +7,9 @@ class EmergencyContactInfoBanner extends StatelessWidget {
@override
Widget build(BuildContext context) {
return UiNoticeBanner(
title:
icon: UiIcons.warning,
title: 'Emergency Contact Information',
description:
'Please provide at least one emergency contact. This information will only be used in case of an emergency during your shifts.',
);
}

View File

@@ -0,0 +1,39 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'contact_field_skeleton.dart';
/// Shimmer placeholder for a single emergency contact card.
class ContactCardSkeleton extends StatelessWidget {
/// Creates a [ContactCardSkeleton].
const ContactCardSkeleton({super.key});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: UiConstants.space4),
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
color: UiColors.bgPopup,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border),
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// Header ("Contact 1")
UiShimmerLine(width: 90, height: 16),
SizedBox(height: UiConstants.space4),
// Full Name field
ContactFieldSkeleton(),
SizedBox(height: UiConstants.space4),
// Phone Number field
ContactFieldSkeleton(),
SizedBox(height: UiConstants.space4),
// Relationship field
ContactFieldSkeleton(),
],
),
);
}
}

View File

@@ -0,0 +1,20 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer placeholder for a single form field (label + input).
class ContactFieldSkeleton extends StatelessWidget {
/// Creates a [ContactFieldSkeleton].
const ContactFieldSkeleton({super.key});
@override
Widget build(BuildContext context) {
return const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UiShimmerLine(width: 80, height: 12),
SizedBox(height: UiConstants.space2),
UiShimmerBox(width: double.infinity, height: 48),
],
);
}
}

View File

@@ -0,0 +1,57 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'contact_card_skeleton.dart';
import 'info_banner_skeleton.dart';
/// Full-page shimmer skeleton shown while emergency contacts are loading.
class EmergencyContactSkeleton extends StatelessWidget {
/// Creates an [EmergencyContactSkeleton].
const EmergencyContactSkeleton({super.key});
@override
Widget build(BuildContext context) {
return UiShimmer(
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(UiConstants.space6),
child: Column(
children: <Widget>[
// Info banner
const InfoBannerSkeleton(),
const SizedBox(height: UiConstants.space6),
// Contact card
const ContactCardSkeleton(),
const SizedBox(height: UiConstants.space4),
// Add contact button placeholder
UiShimmerBox(
width: 180,
height: 40,
borderRadius: UiConstants.radiusFull,
),
const SizedBox(height: UiConstants.space16),
],
),
),
),
// Save button placeholder
Container(
padding: const EdgeInsets.all(UiConstants.space4),
decoration: const BoxDecoration(
border: Border(top: BorderSide(color: UiColors.border)),
),
child: SafeArea(
child: UiShimmerBox(
width: double.infinity,
height: 48,
borderRadius: UiConstants.radiusLg,
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,37 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer placeholder for the emergency contact info banner.
class InfoBannerSkeleton extends StatelessWidget {
/// Creates an [InfoBannerSkeleton].
const InfoBannerSkeleton({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
border: Border.all(color: UiColors.border),
borderRadius: UiConstants.radiusLg,
color: UiColors.cardViewBackground,
),
child: const Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UiShimmerCircle(size: 24),
SizedBox(width: UiConstants.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UiShimmerLine(height: 12),
SizedBox(height: UiConstants.space2),
UiShimmerLine(width: 200, height: 12),
],
),
),
],
),
);
}
}

View File

@@ -7,6 +7,7 @@ import 'package:krow_core/core.dart';
import 'package:staff_profile_info/src/presentation/blocs/personal_info_bloc.dart';
import 'package:staff_profile_info/src/presentation/blocs/personal_info_state.dart';
import 'package:staff_profile_info/src/presentation/widgets/personal_info_page/personal_info_content.dart';
import 'package:staff_profile_info/src/presentation/widgets/personal_info_skeleton/personal_info_skeleton.dart';
/// The Personal Info page for staff onboarding.
@@ -56,7 +57,7 @@ class PersonalInfoPage extends StatelessWidget {
builder: (BuildContext context, PersonalInfoState state) {
if (state.status == PersonalInfoStatus.loading ||
state.status == PersonalInfoStatus.initial) {
return const Center(child: CircularProgressIndicator());
return const PersonalInfoSkeleton();
}
if (state.staff == null) {

View File

@@ -0,0 +1,20 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer placeholder for a single form field (label + input).
class FormFieldSkeleton extends StatelessWidget {
/// Creates a [FormFieldSkeleton].
const FormFieldSkeleton({super.key});
@override
Widget build(BuildContext context) {
return const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UiShimmerLine(width: 80, height: 12),
SizedBox(height: UiConstants.space2),
UiShimmerBox(width: double.infinity, height: 48),
],
);
}
}

View File

@@ -0,0 +1,32 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'form_field_skeleton.dart';
/// Full-page shimmer skeleton shown while personal info is loading.
class PersonalInfoSkeleton extends StatelessWidget {
/// Creates a [PersonalInfoSkeleton].
const PersonalInfoSkeleton({super.key});
@override
Widget build(BuildContext context) {
return UiShimmer(
child: SingleChildScrollView(
padding: const EdgeInsets.all(UiConstants.space5),
child: Column(
children: <Widget>[
// Avatar placeholder
const Center(child: UiShimmerCircle(size: 80)),
const SizedBox(height: UiConstants.space6),
// Form fields
UiShimmerList(
itemCount: 5,
spacing: UiConstants.space5,
itemBuilder: (int index) => const FormFieldSkeleton(),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,29 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer placeholder for a single FAQ accordion item.
class FaqItemSkeleton extends StatelessWidget {
/// Creates a [FaqItemSkeleton].
const FaqItemSkeleton({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
border: Border.all(color: UiColors.border),
borderRadius: UiConstants.radiusLg,
color: UiColors.cardViewBackground,
),
child: const Row(
children: <Widget>[
Expanded(
child: UiShimmerLine(height: 14),
),
SizedBox(width: UiConstants.space3),
UiShimmerCircle(size: 20),
],
),
);
}
}

View File

@@ -0,0 +1,54 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'faq_item_skeleton.dart';
/// Full-page shimmer skeleton shown while FAQs are loading.
class FaqsSkeleton extends StatelessWidget {
/// Creates a [FaqsSkeleton].
const FaqsSkeleton({super.key});
@override
Widget build(BuildContext context) {
return UiShimmer(
child: Padding(
padding: const EdgeInsets.fromLTRB(
UiConstants.space5,
UiConstants.space5,
UiConstants.space5,
UiConstants.space24,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// Search bar placeholder
UiShimmerBox(
width: double.infinity,
height: 48,
borderRadius: UiConstants.radiusLg,
),
const SizedBox(height: UiConstants.space6),
// Category header
const UiShimmerSectionHeader(),
const SizedBox(height: UiConstants.space3),
// FAQ items
UiShimmerList(
itemCount: 3,
spacing: UiConstants.space2,
itemBuilder: (int index) => const FaqItemSkeleton(),
),
const SizedBox(height: UiConstants.space6),
// Second category
const UiShimmerSectionHeader(),
const SizedBox(height: UiConstants.space3),
UiShimmerList(
itemCount: 3,
spacing: UiConstants.space2,
itemBuilder: (int index) => const FaqItemSkeleton(),
),
],
),
),
);
}
}

View File

@@ -3,6 +3,7 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:staff_faqs/src/presentation/blocs/faqs_bloc.dart';
import 'faqs_skeleton/faqs_skeleton.dart';
/// Widget displaying FAQs with search functionality and accordion items
class FaqsWidget extends StatefulWidget {
@@ -76,10 +77,7 @@ class _FaqsWidgetState extends State<FaqsWidget> {
// FAQ List or Empty State
if (state.isLoading)
const Padding(
padding: EdgeInsets.symmetric(vertical: 48),
child: CircularProgressIndicator(),
)
const FaqsSkeleton()
else if (state.categories.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 48),

View File

@@ -17,17 +17,17 @@ class FaqsModule extends Module {
@override
void binds(Injector i) {
// Repository
i.addSingleton<FaqsRepositoryInterface>(
i.addLazySingleton<FaqsRepositoryInterface>(
() => FaqsRepositoryImpl(),
);
// Use Cases
i.addSingleton(
i.addLazySingleton(
() => GetFaqsUseCase(
i<FaqsRepositoryInterface>(),
),
);
i.addSingleton(
i.addLazySingleton(
() => SearchFaqsUseCase(
i<FaqsRepositoryInterface>(),
),

View File

@@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import '../../blocs/legal/privacy_policy_cubit.dart';
import '../../widgets/skeletons/legal_document_skeleton.dart';
/// Page displaying the Privacy Policy document
class PrivacyPolicyPage extends StatelessWidget {
@@ -24,9 +25,7 @@ class PrivacyPolicyPage extends StatelessWidget {
child: BlocBuilder<PrivacyPolicyCubit, PrivacyPolicyState>(
builder: (BuildContext context, PrivacyPolicyState state) {
if (state.isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
return const LegalDocumentSkeleton();
}
if (state.error != null) {

View File

@@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import '../../blocs/legal/terms_cubit.dart';
import '../../widgets/skeletons/legal_document_skeleton.dart';
/// Page displaying the Terms of Service document
class TermsOfServicePage extends StatelessWidget {
@@ -24,9 +25,7 @@ class TermsOfServicePage extends StatelessWidget {
child: BlocBuilder<TermsCubit, TermsState>(
builder: (BuildContext context, TermsState state) {
if (state.isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
return const LegalDocumentSkeleton();
}
if (state.error != null) {

View File

@@ -7,6 +7,7 @@ import 'package:flutter_modular/flutter_modular.dart';
import '../blocs/privacy_security_bloc.dart';
import '../widgets/legal/legal_section_widget.dart';
import '../widgets/privacy/privacy_section_widget.dart';
import '../widgets/skeletons/privacy_security_skeleton.dart';
/// Page displaying privacy & security settings for staff
class PrivacySecurityPage extends StatelessWidget {
@@ -25,7 +26,7 @@ class PrivacySecurityPage extends StatelessWidget {
child: BlocBuilder<PrivacySecurityBloc, PrivacySecurityState>(
builder: (BuildContext context, PrivacySecurityState state) {
if (state.isLoading) {
return const UiLoadingPage();
return const PrivacySecuritySkeleton();
}
return const SingleChildScrollView(

View File

@@ -0,0 +1,61 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shared shimmer skeleton for legal document pages (Privacy Policy, Terms).
///
/// Simulates a long-form text document with varied line widths.
class LegalDocumentSkeleton extends StatelessWidget {
/// Creates a [LegalDocumentSkeleton].
const LegalDocumentSkeleton({super.key});
@override
Widget build(BuildContext context) {
return UiShimmer(
child: SingleChildScrollView(
padding: const EdgeInsets.all(UiConstants.space5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// Title line
const UiShimmerLine(width: 200, height: 18),
const SizedBox(height: UiConstants.space4),
// Body text lines with varied widths
UiShimmerList(
itemCount: 4,
spacing: UiConstants.space2,
itemBuilder: (int index) => const UiShimmerLine(),
),
const SizedBox(height: UiConstants.space5),
const UiShimmerLine(width: 180, height: 16),
const SizedBox(height: UiConstants.space3),
UiShimmerList(
itemCount: 5,
spacing: UiConstants.space2,
itemBuilder: (int index) => UiShimmerLine(
width: index == 4 ? 200 : double.infinity,
),
),
const SizedBox(height: UiConstants.space5),
const UiShimmerLine(width: 160, height: 16),
const SizedBox(height: UiConstants.space3),
UiShimmerList(
itemCount: 3,
spacing: UiConstants.space2,
itemBuilder: (int index) => const UiShimmerLine(),
),
const SizedBox(height: UiConstants.space5),
const UiShimmerLine(width: 140, height: 16),
const SizedBox(height: UiConstants.space3),
UiShimmerList(
itemCount: 4,
spacing: UiConstants.space2,
itemBuilder: (int index) => UiShimmerLine(
width: index == 3 ? 160 : double.infinity,
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,42 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'settings_toggle_skeleton.dart';
/// Full-page shimmer skeleton shown while privacy settings are loading.
class PrivacySecuritySkeleton extends StatelessWidget {
/// Creates a [PrivacySecuritySkeleton].
const PrivacySecuritySkeleton({super.key});
@override
Widget build(BuildContext context) {
return UiShimmer(
child: SingleChildScrollView(
padding: const EdgeInsets.all(UiConstants.space6),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// Privacy section header
const UiShimmerSectionHeader(),
const SizedBox(height: UiConstants.space4),
UiShimmerList(
itemCount: 3,
spacing: UiConstants.space4,
itemBuilder: (int index) => const SettingsToggleSkeleton(),
),
const SizedBox(height: UiConstants.space6),
// Legal section header
const UiShimmerSectionHeader(),
const SizedBox(height: UiConstants.space4),
// Legal links
UiShimmerList(
itemCount: 2,
spacing: UiConstants.space3,
itemBuilder: (int index) => const UiShimmerListItem(),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,31 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Shimmer placeholder for a single settings toggle row.
class SettingsToggleSkeleton extends StatelessWidget {
/// Creates a [SettingsToggleSkeleton].
const SettingsToggleSkeleton({super.key});
@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: UiConstants.space2),
child: Row(
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UiShimmerLine(width: 140, height: 14),
SizedBox(height: UiConstants.space1),
UiShimmerLine(width: 200, height: 12),
],
),
),
SizedBox(width: UiConstants.space3),
UiShimmerBox(width: 48, height: 28),
],
),
);
}
}

View File

@@ -25,29 +25,29 @@ class PrivacySecurityModule extends Module {
@override
void binds(Injector i) {
// Repository
i.addSingleton<PrivacySettingsRepositoryInterface>(
i.addLazySingleton<PrivacySettingsRepositoryInterface>(
() => PrivacySettingsRepositoryImpl(
Modular.get<DataConnectService>(),
),
);
// Use Cases
i.addSingleton(
i.addLazySingleton(
() => GetProfileVisibilityUseCase(
i<PrivacySettingsRepositoryInterface>(),
),
);
i.addSingleton(
i.addLazySingleton(
() => UpdateProfileVisibilityUseCase(
i<PrivacySettingsRepositoryInterface>(),
),
);
i.addSingleton(
i.addLazySingleton(
() => GetTermsUseCase(
i<PrivacySettingsRepositoryInterface>(),
),
);
i.addSingleton(
i.addLazySingleton(
() => GetPrivacyPolicyUseCase(
i<PrivacySettingsRepositoryInterface>(),
),