feat: Refactor client home widgets to use SectionLayout and add titles
- Updated ActionsWidget, CoverageWidget, SpendingWidget, ReorderWidget, and LiveActivityWidget to utilize SectionLayout for consistent layout structure. - Introduced SectionHeader for displaying titles and optional actions in sections. - Added ClientHomeBody, ClientHomeEditModeBody, ClientHomeNormalModeBody, and ClientHomeErrorState for improved state management and UI separation. - Enhanced dashboard widget builder to support edit mode and error handling.
This commit is contained in:
@@ -1,15 +1,12 @@
|
|||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
|
||||||
import '../blocs/client_home_bloc.dart';
|
import '../blocs/client_home_bloc.dart';
|
||||||
import '../blocs/client_home_event.dart';
|
import '../widgets/client_home_body.dart';
|
||||||
import '../blocs/client_home_state.dart';
|
|
||||||
import '../widgets/client_home_edit_banner.dart';
|
import '../widgets/client_home_edit_banner.dart';
|
||||||
import '../widgets/client_home_header.dart';
|
import '../widgets/client_home_header.dart';
|
||||||
import '../widgets/dashboard_widget_builder.dart';
|
|
||||||
|
|
||||||
/// The main Home page for client users.
|
/// The main Home page for client users.
|
||||||
///
|
///
|
||||||
@@ -21,133 +18,23 @@ class ClientHomePage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final TranslationsClientHomeEn i18n = t.client_home;
|
|
||||||
|
|
||||||
return BlocProvider<ClientHomeBloc>(
|
return BlocProvider<ClientHomeBloc>(
|
||||||
create: (BuildContext context) => Modular.get<ClientHomeBloc>(),
|
create: (BuildContext context) => Modular.get<ClientHomeBloc>(),
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ClientHomeHeader(i18n: i18n),
|
ClientHomeHeader(
|
||||||
ClientHomeEditBanner(i18n: i18n),
|
i18n: t.client_home,
|
||||||
Flexible(
|
|
||||||
child: BlocConsumer<ClientHomeBloc, ClientHomeState>(
|
|
||||||
listener: (BuildContext context, ClientHomeState state) {
|
|
||||||
if (state.status == ClientHomeStatus.error &&
|
|
||||||
state.errorMessage != null) {
|
|
||||||
UiSnackbar.show(
|
|
||||||
context,
|
|
||||||
message: translateErrorKey(state.errorMessage!),
|
|
||||||
type: UiSnackbarType.error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
builder: (BuildContext context, ClientHomeState state) {
|
|
||||||
if (state.status == ClientHomeStatus.error) {
|
|
||||||
return _buildErrorState(context, state);
|
|
||||||
}
|
|
||||||
if (state.isEditMode) {
|
|
||||||
return _buildEditModeList(context, state);
|
|
||||||
}
|
|
||||||
return _buildNormalModeList(state);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
ClientHomeEditBanner(
|
||||||
|
i18n: t.client_home,
|
||||||
|
),
|
||||||
|
const ClientHomeBody(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds the widget list in edit mode with drag-and-drop support.
|
|
||||||
Widget _buildEditModeList(BuildContext context, ClientHomeState state) {
|
|
||||||
return ReorderableListView(
|
|
||||||
padding: const EdgeInsets.fromLTRB(
|
|
||||||
UiConstants.space4,
|
|
||||||
0,
|
|
||||||
UiConstants.space4,
|
|
||||||
100,
|
|
||||||
),
|
|
||||||
onReorder: (int oldIndex, int newIndex) {
|
|
||||||
BlocProvider.of<ClientHomeBloc>(
|
|
||||||
context,
|
|
||||||
).add(ClientHomeWidgetReordered(oldIndex, newIndex));
|
|
||||||
},
|
|
||||||
children: state.widgetOrder.map((String id) {
|
|
||||||
return Container(
|
|
||||||
key: ValueKey<String>(id),
|
|
||||||
margin: const EdgeInsets.only(bottom: UiConstants.space4),
|
|
||||||
child: DashboardWidgetBuilder(id: id, state: state, isEditMode: true),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds the widget list in normal mode with visibility filters.
|
|
||||||
Widget _buildNormalModeList(ClientHomeState state) {
|
|
||||||
final List<String> visibleWidgets = state.widgetOrder.where((String id) {
|
|
||||||
if (id == 'reorder' && state.reorderItems.isEmpty) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return state.widgetVisibility[id] ?? true;
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
return ListView.separated(
|
|
||||||
separatorBuilder: (BuildContext context, int index) {
|
|
||||||
return const Divider(color: UiColors.border, height: 0.1);
|
|
||||||
},
|
|
||||||
itemCount: visibleWidgets.length,
|
|
||||||
itemBuilder: (BuildContext context, int index) {
|
|
||||||
final String id = visibleWidgets[index];
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
if (index != 0) const SizedBox(height: UiConstants.space8),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: UiConstants.space4,
|
|
||||||
),
|
|
||||||
child: DashboardWidgetBuilder(
|
|
||||||
id: id,
|
|
||||||
state: state,
|
|
||||||
isEditMode: false,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space8),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildErrorState(BuildContext context, ClientHomeState state) {
|
|
||||||
return Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
const Icon(
|
|
||||||
UiIcons.error,
|
|
||||||
size: 48,
|
|
||||||
color: UiColors.error,
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space4),
|
|
||||||
Text(
|
|
||||||
state.errorMessage != null
|
|
||||||
? translateErrorKey(state.errorMessage!)
|
|
||||||
: 'An error occurred',
|
|
||||||
style: UiTypography.body1m.textError,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space4),
|
|
||||||
UiButton.secondary(
|
|
||||||
text: 'Retry',
|
|
||||||
onPressed: () =>
|
|
||||||
BlocProvider.of<ClientHomeBloc>(context).add(ClientHomeStarted()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,19 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
|
import 'section_layout.dart';
|
||||||
|
|
||||||
/// A widget that displays quick actions for the client.
|
/// A widget that displays quick actions for the client.
|
||||||
class ActionsWidget extends StatelessWidget {
|
class ActionsWidget extends StatelessWidget {
|
||||||
/// Creates an [ActionsWidget].
|
/// Creates an [ActionsWidget].
|
||||||
const ActionsWidget({super.key, this.subtitle});
|
const ActionsWidget({
|
||||||
|
super.key,
|
||||||
|
this.title,
|
||||||
|
this.subtitle,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Optional title for the section.
|
||||||
|
final String? title;
|
||||||
|
|
||||||
/// Optional subtitle for the section.
|
/// Optional subtitle for the section.
|
||||||
final String? subtitle;
|
final String? subtitle;
|
||||||
@@ -17,38 +26,41 @@ class ActionsWidget extends StatelessWidget {
|
|||||||
// Check if client_home exists in t
|
// Check if client_home exists in t
|
||||||
final TranslationsClientHomeActionsEn i18n = t.client_home.actions;
|
final TranslationsClientHomeActionsEn i18n = t.client_home.actions;
|
||||||
|
|
||||||
return Row(
|
return SectionLayout(
|
||||||
spacing: UiConstants.space4,
|
title: title,
|
||||||
children: <Widget>[
|
child: Row(
|
||||||
Expanded(
|
spacing: UiConstants.space4,
|
||||||
child: _ActionCard(
|
children: <Widget>[
|
||||||
title: i18n.rapid,
|
Expanded(
|
||||||
subtitle: i18n.rapid_subtitle,
|
child: _ActionCard(
|
||||||
icon: UiIcons.zap,
|
title: i18n.rapid,
|
||||||
color: UiColors.tagError.withValues(alpha: 0.5),
|
subtitle: i18n.rapid_subtitle,
|
||||||
borderColor: UiColors.borderError.withValues(alpha: 0.3),
|
icon: UiIcons.zap,
|
||||||
iconBgColor: UiColors.white,
|
color: UiColors.tagError.withValues(alpha: 0.5),
|
||||||
iconColor: UiColors.textError,
|
borderColor: UiColors.borderError.withValues(alpha: 0.3),
|
||||||
textColor: UiColors.textError,
|
iconBgColor: UiColors.white,
|
||||||
subtitleColor: UiColors.textError.withValues(alpha: 0.8),
|
iconColor: UiColors.textError,
|
||||||
onTap: () => Modular.to.toCreateOrderRapid(),
|
textColor: UiColors.textError,
|
||||||
|
subtitleColor: UiColors.textError.withValues(alpha: 0.8),
|
||||||
|
onTap: () => Modular.to.toCreateOrderRapid(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
Expanded(
|
||||||
Expanded(
|
child: _ActionCard(
|
||||||
child: _ActionCard(
|
title: i18n.create_order,
|
||||||
title: i18n.create_order,
|
subtitle: i18n.create_order_subtitle,
|
||||||
subtitle: i18n.create_order_subtitle,
|
icon: UiIcons.add,
|
||||||
icon: UiIcons.add,
|
color: UiColors.white,
|
||||||
color: UiColors.white,
|
borderColor: UiColors.border,
|
||||||
borderColor: UiColors.border,
|
iconBgColor: UiColors.primaryForeground,
|
||||||
iconBgColor: UiColors.primaryForeground,
|
iconColor: UiColors.primary,
|
||||||
iconColor: UiColors.primary,
|
textColor: UiColors.textPrimary,
|
||||||
textColor: UiColors.textPrimary,
|
subtitleColor: UiColors.textSecondary,
|
||||||
subtitleColor: UiColors.textSecondary,
|
onTap: () => Modular.to.toCreateOrder(),
|
||||||
onTap: () => Modular.to.toCreateOrder(),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
import '../blocs/client_home_bloc.dart';
|
||||||
|
import '../blocs/client_home_state.dart';
|
||||||
|
import 'client_home_edit_mode_body.dart';
|
||||||
|
import 'client_home_error_state.dart';
|
||||||
|
import 'client_home_normal_mode_body.dart';
|
||||||
|
|
||||||
|
/// Main body widget for the client home page.
|
||||||
|
///
|
||||||
|
/// Manages the state transitions between error, edit mode, and normal mode views.
|
||||||
|
class ClientHomeBody extends StatelessWidget {
|
||||||
|
/// Creates a [ClientHomeBody].
|
||||||
|
const ClientHomeBody({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Flexible(
|
||||||
|
child: BlocConsumer<ClientHomeBloc, ClientHomeState>(
|
||||||
|
listener: (BuildContext context, ClientHomeState state) {
|
||||||
|
if (state.status == ClientHomeStatus.error &&
|
||||||
|
state.errorMessage != null) {
|
||||||
|
UiSnackbar.show(
|
||||||
|
context,
|
||||||
|
message: translateErrorKey(state.errorMessage!),
|
||||||
|
type: UiSnackbarType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
builder: (BuildContext context, ClientHomeState state) {
|
||||||
|
if (state.status == ClientHomeStatus.error) {
|
||||||
|
return ClientHomeErrorState(state: state);
|
||||||
|
}
|
||||||
|
if (state.isEditMode) {
|
||||||
|
return ClientHomeEditModeBody(state: state);
|
||||||
|
}
|
||||||
|
return ClientHomeNormalModeBody(state: state);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
import '../blocs/client_home_bloc.dart';
|
||||||
|
import '../blocs/client_home_event.dart';
|
||||||
|
import '../blocs/client_home_state.dart';
|
||||||
|
import 'dashboard_widget_builder.dart';
|
||||||
|
|
||||||
|
/// Widget that displays the home dashboard in edit mode with drag-and-drop support.
|
||||||
|
///
|
||||||
|
/// Allows users to reorder and rearrange dashboard widgets.
|
||||||
|
class ClientHomeEditModeBody extends StatelessWidget {
|
||||||
|
/// The current home state.
|
||||||
|
final ClientHomeState state;
|
||||||
|
|
||||||
|
/// Creates a [ClientHomeEditModeBody].
|
||||||
|
const ClientHomeEditModeBody({
|
||||||
|
required this.state,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ReorderableListView(
|
||||||
|
padding: const EdgeInsets.fromLTRB(
|
||||||
|
UiConstants.space4,
|
||||||
|
0,
|
||||||
|
UiConstants.space4,
|
||||||
|
100,
|
||||||
|
),
|
||||||
|
onReorder: (int oldIndex, int newIndex) {
|
||||||
|
BlocProvider.of<ClientHomeBloc>(context)
|
||||||
|
.add(ClientHomeWidgetReordered(oldIndex, newIndex));
|
||||||
|
},
|
||||||
|
children: state.widgetOrder.map((String id) {
|
||||||
|
return Container(
|
||||||
|
key: ValueKey<String>(id),
|
||||||
|
margin: const EdgeInsets.only(bottom: UiConstants.space4),
|
||||||
|
child: DashboardWidgetBuilder(
|
||||||
|
id: id,
|
||||||
|
state: state,
|
||||||
|
isEditMode: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
import '../blocs/client_home_bloc.dart';
|
||||||
|
import '../blocs/client_home_event.dart';
|
||||||
|
import '../blocs/client_home_state.dart';
|
||||||
|
|
||||||
|
/// Widget that displays an error state for the client home page.
|
||||||
|
///
|
||||||
|
/// Shows an error message with a retry button when data fails to load.
|
||||||
|
class ClientHomeErrorState extends StatelessWidget {
|
||||||
|
/// The current home state containing error information.
|
||||||
|
final ClientHomeState state;
|
||||||
|
|
||||||
|
/// Creates a [ClientHomeErrorState].
|
||||||
|
const ClientHomeErrorState({
|
||||||
|
required this.state,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
const Icon(
|
||||||
|
UiIcons.error,
|
||||||
|
size: 48,
|
||||||
|
color: UiColors.error,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
Text(
|
||||||
|
state.errorMessage != null
|
||||||
|
? translateErrorKey(state.errorMessage!)
|
||||||
|
: 'An error occurred',
|
||||||
|
style: UiTypography.body1m.textError,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
UiButton.secondary(
|
||||||
|
text: 'Retry',
|
||||||
|
onPressed: () =>
|
||||||
|
BlocProvider.of<ClientHomeBloc>(context).add(ClientHomeStarted()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../blocs/client_home_state.dart';
|
||||||
|
import 'dashboard_widget_builder.dart';
|
||||||
|
|
||||||
|
/// Widget that displays the home dashboard in normal mode.
|
||||||
|
///
|
||||||
|
/// Shows visible dashboard widgets in a vertical scrollable list with dividers.
|
||||||
|
class ClientHomeNormalModeBody extends StatelessWidget {
|
||||||
|
/// The current home state.
|
||||||
|
final ClientHomeState state;
|
||||||
|
|
||||||
|
/// Creates a [ClientHomeNormalModeBody].
|
||||||
|
const ClientHomeNormalModeBody({
|
||||||
|
required this.state,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final List<String> visibleWidgets = state.widgetOrder.where((String id) {
|
||||||
|
if (id == 'reorder' && state.reorderItems.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return state.widgetVisibility[id] ?? true;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
return ListView.separated(
|
||||||
|
separatorBuilder: (BuildContext context, int index) {
|
||||||
|
return const Divider(color: UiColors.border, height: 0.1);
|
||||||
|
},
|
||||||
|
itemCount: visibleWidgets.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
final String id = visibleWidgets[index];
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
if (index != 0) const SizedBox(height: UiConstants.space8),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: UiConstants.space4,
|
||||||
|
),
|
||||||
|
child: DashboardWidgetBuilder(
|
||||||
|
id: id,
|
||||||
|
state: state,
|
||||||
|
isEditMode: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space8),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import 'package:core_localization/core_localization.dart';
|
|||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'section_layout.dart';
|
||||||
|
|
||||||
/// A widget that displays the daily coverage metrics.
|
/// A widget that displays the daily coverage metrics.
|
||||||
class CoverageWidget extends StatelessWidget {
|
class CoverageWidget extends StatelessWidget {
|
||||||
/// Creates a [CoverageWidget].
|
/// Creates a [CoverageWidget].
|
||||||
@@ -10,6 +12,7 @@ class CoverageWidget extends StatelessWidget {
|
|||||||
this.totalNeeded = 0,
|
this.totalNeeded = 0,
|
||||||
this.totalConfirmed = 0,
|
this.totalConfirmed = 0,
|
||||||
this.coveragePercent = 0,
|
this.coveragePercent = 0,
|
||||||
|
this.title,
|
||||||
this.subtitle,
|
this.subtitle,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -22,84 +25,42 @@ class CoverageWidget extends StatelessWidget {
|
|||||||
/// The percentage of coverage (0-100).
|
/// The percentage of coverage (0-100).
|
||||||
final int coveragePercent;
|
final int coveragePercent;
|
||||||
|
|
||||||
|
/// Optional title for the section.
|
||||||
|
final String? title;
|
||||||
|
|
||||||
/// Optional subtitle for the section.
|
/// Optional subtitle for the section.
|
||||||
final String? subtitle;
|
final String? subtitle;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Color backgroundColor;
|
return SectionLayout(
|
||||||
Color textColor;
|
title: title,
|
||||||
|
action: totalNeeded > 0 || totalConfirmed > 0 || coveragePercent > 0
|
||||||
if (coveragePercent == 100) {
|
? t.client_home.dashboard.percent_covered(percent: coveragePercent)
|
||||||
backgroundColor = UiColors.tagActive;
|
: null,
|
||||||
textColor = UiColors.textSuccess;
|
child: Row(
|
||||||
} else if (coveragePercent >= 40) {
|
children: <Widget>[
|
||||||
backgroundColor = UiColors.tagPending;
|
Expanded(
|
||||||
textColor = UiColors.textWarning;
|
child: _MetricCard(
|
||||||
} else {
|
icon: UiIcons.target,
|
||||||
backgroundColor = UiColors.tagError;
|
iconColor: UiColors.primary,
|
||||||
textColor = UiColors.textError;
|
label: t.client_home.dashboard.metric_needed,
|
||||||
}
|
value: '$totalNeeded',
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
t.client_home.dashboard.todays_coverage,
|
|
||||||
style: UiTypography.footnote1b.copyWith(
|
|
||||||
color: UiColors.textPrimary,
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (totalNeeded > 0 || totalConfirmed > 0 || coveragePercent > 0)
|
),
|
||||||
Container(
|
const SizedBox(width: UiConstants.space2),
|
||||||
padding: const EdgeInsets.symmetric(
|
if (totalConfirmed != 0)
|
||||||
horizontal: UiConstants.space2,
|
|
||||||
vertical:
|
|
||||||
2, // 2px is not in metrics, using hardcoded for small tweaks or space0/space1
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: backgroundColor,
|
|
||||||
borderRadius: UiConstants.radiusLg,
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
t.client_home.dashboard.percent_covered(
|
|
||||||
percent: coveragePercent,
|
|
||||||
),
|
|
||||||
style: UiTypography.footnote2b.copyWith(color: textColor),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (subtitle != null) ...<Widget>[
|
|
||||||
Text(subtitle!, style: UiTypography.body2r.textSecondary),
|
|
||||||
],
|
|
||||||
const SizedBox(height: UiConstants.space6),
|
|
||||||
Row(
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _MetricCard(
|
child: _MetricCard(
|
||||||
icon: UiIcons.target,
|
icon: UiIcons.success,
|
||||||
iconColor: UiColors.primary,
|
iconColor: UiColors.iconSuccess,
|
||||||
label: t.client_home.dashboard.metric_needed,
|
label: t.client_home.dashboard.metric_filled,
|
||||||
value: '$totalNeeded',
|
value: '$totalConfirmed',
|
||||||
|
valueColor: UiColors.textSuccess,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: UiConstants.space2),
|
const SizedBox(width: UiConstants.space2),
|
||||||
if (totalConfirmed != 0)
|
if (totalConfirmed != 0)
|
||||||
Expanded(
|
|
||||||
child: _MetricCard(
|
|
||||||
icon: UiIcons.success,
|
|
||||||
iconColor: UiColors.iconSuccess,
|
|
||||||
label: t.client_home.dashboard.metric_filled,
|
|
||||||
value: '$totalConfirmed',
|
|
||||||
valueColor: UiColors.textSuccess,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space2),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _MetricCard(
|
child: _MetricCard(
|
||||||
icon: UiIcons.error,
|
icon: UiIcons.error,
|
||||||
@@ -109,9 +70,8 @@ class CoverageWidget extends StatelessWidget {
|
|||||||
valueColor: UiColors.textError,
|
valueColor: UiColors.textError,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class DashboardWidgetBuilder extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final TranslationsClientHomeWidgetsEn i18n = t.client_home.widgets;
|
final TranslationsClientHomeWidgetsEn i18n = t.client_home.widgets;
|
||||||
final Widget widgetContent = _buildWidgetContent(context);
|
final Widget widgetContent = _buildWidgetContent(context, i18n);
|
||||||
|
|
||||||
if (isEditMode) {
|
if (isEditMode) {
|
||||||
return DraggableWidgetWrapper(
|
return DraggableWidgetWrapper(
|
||||||
@@ -55,21 +55,27 @@ class DashboardWidgetBuilder extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Builds the actual widget content based on the widget ID.
|
/// Builds the actual widget content based on the widget ID.
|
||||||
Widget _buildWidgetContent(BuildContext context) {
|
Widget _buildWidgetContent(BuildContext context, TranslationsClientHomeWidgetsEn i18n) {
|
||||||
|
final String title = _getWidgetTitle(i18n);
|
||||||
// Only show subtitle in normal mode
|
// Only show subtitle in normal mode
|
||||||
final String? subtitle = !isEditMode ? _getWidgetSubtitle(id) : null;
|
final String? subtitle = !isEditMode ? _getWidgetSubtitle(id) : null;
|
||||||
|
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case 'actions':
|
case 'actions':
|
||||||
return ActionsWidget(subtitle: subtitle);
|
return ActionsWidget(title: title, subtitle: subtitle);
|
||||||
case 'reorder':
|
case 'reorder':
|
||||||
return ReorderWidget(orders: state.reorderItems, subtitle: subtitle);
|
return ReorderWidget(
|
||||||
|
orders: state.reorderItems,
|
||||||
|
title: title,
|
||||||
|
subtitle: subtitle,
|
||||||
|
);
|
||||||
case 'spending':
|
case 'spending':
|
||||||
return SpendingWidget(
|
return SpendingWidget(
|
||||||
weeklySpending: state.dashboardData.weeklySpending,
|
weeklySpending: state.dashboardData.weeklySpending,
|
||||||
next7DaysSpending: state.dashboardData.next7DaysSpending,
|
next7DaysSpending: state.dashboardData.next7DaysSpending,
|
||||||
weeklyShifts: state.dashboardData.weeklyShifts,
|
weeklyShifts: state.dashboardData.weeklyShifts,
|
||||||
next7DaysScheduled: state.dashboardData.next7DaysScheduled,
|
next7DaysScheduled: state.dashboardData.next7DaysScheduled,
|
||||||
|
title: title,
|
||||||
subtitle: subtitle,
|
subtitle: subtitle,
|
||||||
);
|
);
|
||||||
case 'coverage':
|
case 'coverage':
|
||||||
@@ -82,11 +88,13 @@ class DashboardWidgetBuilder extends StatelessWidget {
|
|||||||
100)
|
100)
|
||||||
.toInt()
|
.toInt()
|
||||||
: 0,
|
: 0,
|
||||||
|
title: title,
|
||||||
subtitle: subtitle,
|
subtitle: subtitle,
|
||||||
);
|
);
|
||||||
case 'liveActivity':
|
case 'liveActivity':
|
||||||
return LiveActivityWidget(
|
return LiveActivityWidget(
|
||||||
onViewAllPressed: () => Modular.to.toClientCoverage(),
|
onViewAllPressed: () => Modular.to.toClientCoverage(),
|
||||||
|
title: title,
|
||||||
subtitle: subtitle,
|
subtitle: subtitle,
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
|
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||||
|
|
||||||
import 'coverage_dashboard.dart';
|
import 'coverage_dashboard.dart';
|
||||||
|
import 'section_layout.dart';
|
||||||
|
|
||||||
/// A widget that displays live activity information.
|
/// A widget that displays live activity information.
|
||||||
class LiveActivityWidget extends StatefulWidget {
|
class LiveActivityWidget extends StatefulWidget {
|
||||||
@@ -12,11 +13,15 @@ class LiveActivityWidget extends StatefulWidget {
|
|||||||
const LiveActivityWidget({
|
const LiveActivityWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.onViewAllPressed,
|
required this.onViewAllPressed,
|
||||||
|
this.title,
|
||||||
this.subtitle
|
this.subtitle
|
||||||
});
|
});
|
||||||
/// Callback when "View all" is pressed.
|
/// Callback when "View all" is pressed.
|
||||||
final VoidCallback onViewAllPressed;
|
final VoidCallback onViewAllPressed;
|
||||||
|
|
||||||
|
/// Optional title for the section.
|
||||||
|
final String? title;
|
||||||
|
|
||||||
/// Optional subtitle for the section.
|
/// Optional subtitle for the section.
|
||||||
final String? subtitle;
|
final String? subtitle;
|
||||||
|
|
||||||
@@ -106,73 +111,46 @@ class _LiveActivityWidgetState extends State<LiveActivityWidget> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final TranslationsClientHomeEn i18n = t.client_home;
|
final TranslationsClientHomeEn i18n = t.client_home;
|
||||||
|
|
||||||
return Column(
|
return SectionLayout(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
title: widget.title,
|
||||||
children: <Widget>[
|
action: i18n.dashboard.view_all,
|
||||||
Row(
|
onAction: widget.onViewAllPressed,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
child: FutureBuilder<_LiveActivityData>(
|
||||||
children: <Widget>[
|
future: _liveActivityFuture,
|
||||||
Text(
|
builder: (BuildContext context,
|
||||||
i18n.widgets.live_activity.toUpperCase(),
|
AsyncSnapshot<_LiveActivityData> snapshot) {
|
||||||
style: UiTypography.footnote1b.textSecondary.copyWith(
|
final _LiveActivityData data =
|
||||||
letterSpacing: 0.5,
|
snapshot.data ?? _LiveActivityData.empty();
|
||||||
),
|
final List<Map<String, Object>> shifts =
|
||||||
),
|
<Map<String, Object>>[
|
||||||
GestureDetector(
|
<String, Object>{
|
||||||
onTap: widget.onViewAllPressed,
|
'workersNeeded': data.totalNeeded,
|
||||||
child: Text(
|
'filled': data.totalAssigned,
|
||||||
i18n.dashboard.view_all,
|
'hourlyRate': 1.0,
|
||||||
style: UiTypography.footnote1m.copyWith(
|
'hours': data.totalCost,
|
||||||
color: UiColors.primary,
|
'status': 'OPEN',
|
||||||
),
|
'date': DateTime.now().toIso8601String().split('T')[0],
|
||||||
),
|
},
|
||||||
),
|
];
|
||||||
],
|
final List<Map<String, Object?>> applications =
|
||||||
),
|
<Map<String, Object?>>[];
|
||||||
if (widget.subtitle != null) ...<Widget>[
|
for (int i = 0; i < data.checkedInCount; i += 1) {
|
||||||
Text(
|
applications.add(
|
||||||
widget.subtitle!,
|
<String, Object?>{
|
||||||
style: UiTypography.body2r.textSecondary,
|
'status': 'CONFIRMED',
|
||||||
),
|
'checkInTime': '09:00',
|
||||||
],
|
|
||||||
const SizedBox(height: UiConstants.space6),
|
|
||||||
FutureBuilder<_LiveActivityData>(
|
|
||||||
future: _liveActivityFuture,
|
|
||||||
builder: (BuildContext context,
|
|
||||||
AsyncSnapshot<_LiveActivityData> snapshot) {
|
|
||||||
final _LiveActivityData data =
|
|
||||||
snapshot.data ?? _LiveActivityData.empty();
|
|
||||||
final List<Map<String, Object>> shifts =
|
|
||||||
<Map<String, Object>>[
|
|
||||||
<String, Object>{
|
|
||||||
'workersNeeded': data.totalNeeded,
|
|
||||||
'filled': data.totalAssigned,
|
|
||||||
'hourlyRate': 1.0,
|
|
||||||
'hours': data.totalCost,
|
|
||||||
'status': 'OPEN',
|
|
||||||
'date': DateTime.now().toIso8601String().split('T')[0],
|
|
||||||
},
|
},
|
||||||
];
|
|
||||||
final List<Map<String, Object?>> applications =
|
|
||||||
<Map<String, Object?>>[];
|
|
||||||
for (int i = 0; i < data.checkedInCount; i += 1) {
|
|
||||||
applications.add(
|
|
||||||
<String, Object?>{
|
|
||||||
'status': 'CONFIRMED',
|
|
||||||
'checkInTime': '09:00',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for (int i = 0; i < data.lateCount; i += 1) {
|
|
||||||
applications.add(<String, Object?>{'status': 'LATE'});
|
|
||||||
}
|
|
||||||
return CoverageDashboard(
|
|
||||||
shifts: shifts,
|
|
||||||
applications: applications,
|
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
),
|
for (int i = 0; i < data.lateCount; i += 1) {
|
||||||
],
|
applications.add(<String, Object?>{'status': 'LATE'});
|
||||||
|
}
|
||||||
|
return CoverageDashboard(
|
||||||
|
shifts: shifts,
|
||||||
|
applications: applications,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,24 @@ import 'package:flutter_modular/flutter_modular.dart';
|
|||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
import 'section_layout.dart';
|
||||||
|
|
||||||
/// A widget that allows clients to reorder recent shifts.
|
/// A widget that allows clients to reorder recent shifts.
|
||||||
class ReorderWidget extends StatelessWidget {
|
class ReorderWidget extends StatelessWidget {
|
||||||
/// Creates a [ReorderWidget].
|
/// Creates a [ReorderWidget].
|
||||||
const ReorderWidget({super.key, required this.orders, this.subtitle});
|
const ReorderWidget({
|
||||||
|
super.key,
|
||||||
|
required this.orders,
|
||||||
|
this.title,
|
||||||
|
this.subtitle,
|
||||||
|
});
|
||||||
|
|
||||||
/// Recent completed orders for reorder.
|
/// Recent completed orders for reorder.
|
||||||
final List<ReorderItem> orders;
|
final List<ReorderItem> orders;
|
||||||
|
|
||||||
|
/// Optional title for the section.
|
||||||
|
final String? title;
|
||||||
|
|
||||||
/// Optional subtitle for the section.
|
/// Optional subtitle for the section.
|
||||||
final String? subtitle;
|
final String? subtitle;
|
||||||
|
|
||||||
@@ -26,147 +36,134 @@ class ReorderWidget extends StatelessWidget {
|
|||||||
|
|
||||||
final List<ReorderItem> recentOrders = orders;
|
final List<ReorderItem> recentOrders = orders;
|
||||||
|
|
||||||
return Column(
|
return SectionLayout(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
title: title,
|
||||||
children: <Widget>[
|
child: SizedBox(
|
||||||
Text(
|
height: 164,
|
||||||
i18n.title,
|
child: ListView.separated(
|
||||||
style: UiTypography.footnote1b.textSecondary.copyWith(
|
scrollDirection: Axis.horizontal,
|
||||||
letterSpacing: 0.5,
|
itemCount: recentOrders.length,
|
||||||
),
|
separatorBuilder: (BuildContext context, int index) =>
|
||||||
),
|
const SizedBox(width: UiConstants.space3),
|
||||||
if (subtitle != null) ...<Widget>[
|
itemBuilder: (BuildContext context, int index) {
|
||||||
const SizedBox(height: UiConstants.space1),
|
final ReorderItem order = recentOrders[index];
|
||||||
Text(subtitle!, style: UiTypography.body2r.textSecondary),
|
final double totalCost = order.totalCost;
|
||||||
],
|
|
||||||
const SizedBox(height: UiConstants.space2),
|
|
||||||
SizedBox(
|
|
||||||
height: 164,
|
|
||||||
child: ListView.separated(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
itemCount: recentOrders.length,
|
|
||||||
separatorBuilder: (BuildContext context, int index) =>
|
|
||||||
const SizedBox(width: UiConstants.space3),
|
|
||||||
itemBuilder: (BuildContext context, int index) {
|
|
||||||
final ReorderItem order = recentOrders[index];
|
|
||||||
final double totalCost = order.totalCost;
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: 260,
|
width: 260,
|
||||||
padding: const EdgeInsets.all(UiConstants.space3),
|
padding: const EdgeInsets.all(UiConstants.space3),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.white,
|
color: UiColors.white,
|
||||||
borderRadius: UiConstants.radiusLg,
|
borderRadius: UiConstants.radiusLg,
|
||||||
border: Border.all(color: UiColors.border, width: 0.6),
|
border: Border.all(color: UiColors.border, width: 0.6),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
|
||||||
Container(
|
|
||||||
width: 36,
|
|
||||||
height: 36,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: UiColors.primary.withValues(
|
|
||||||
alpha: 0.1,
|
|
||||||
),
|
|
||||||
borderRadius: UiConstants.radiusLg,
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
UiIcons.building,
|
|
||||||
size: 16,
|
|
||||||
color: UiColors.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space2),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
order.title,
|
|
||||||
style: UiTypography.body2b,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
order.location,
|
|
||||||
style:
|
|
||||||
UiTypography.footnote1r.textSecondary,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Container(
|
||||||
'\$${totalCost.toStringAsFixed(0)}',
|
width: 36,
|
||||||
style: UiTypography.body1b,
|
height: 36,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.primary.withValues(
|
||||||
|
alpha: 0.1,
|
||||||
|
),
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
UiIcons.building,
|
||||||
|
size: 16,
|
||||||
|
color: UiColors.primary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Text(
|
const SizedBox(width: UiConstants.space2),
|
||||||
'${i18n.per_hr(amount: order.hourlyRate.toString())} · ${order.hours}h',
|
Expanded(
|
||||||
style: UiTypography.footnote2r.textSecondary,
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
order.title,
|
||||||
|
style: UiTypography.body2b,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
order.location,
|
||||||
|
style:
|
||||||
|
UiTypography.footnote1r.textSecondary,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
Column(
|
||||||
const SizedBox(height: UiConstants.space3),
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
Row(
|
children: <Widget>[
|
||||||
children: <Widget>[
|
Text(
|
||||||
_Badge(
|
'\$${totalCost.toStringAsFixed(0)}',
|
||||||
icon: UiIcons.success,
|
style: UiTypography.body1b,
|
||||||
text: order.type,
|
),
|
||||||
color: UiColors.primary,
|
Text(
|
||||||
bg: UiColors.buttonSecondaryStill,
|
'${i18n.per_hr(amount: order.hourlyRate.toString())} · ${order.hours}h',
|
||||||
textColor: UiColors.primary,
|
style: UiTypography.footnote2r.textSecondary,
|
||||||
),
|
),
|
||||||
const SizedBox(width: UiConstants.space2),
|
],
|
||||||
_Badge(
|
),
|
||||||
icon: UiIcons.building,
|
],
|
||||||
text: '${order.workers}',
|
),
|
||||||
color: UiColors.textSecondary,
|
const SizedBox(height: UiConstants.space3),
|
||||||
bg: UiColors.buttonSecondaryStill,
|
Row(
|
||||||
textColor: UiColors.textSecondary,
|
children: <Widget>[
|
||||||
),
|
_Badge(
|
||||||
],
|
icon: UiIcons.success,
|
||||||
),
|
text: order.type,
|
||||||
const Spacer(),
|
color: UiColors.primary,
|
||||||
|
bg: UiColors.buttonSecondaryStill,
|
||||||
|
textColor: UiColors.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
_Badge(
|
||||||
|
icon: UiIcons.building,
|
||||||
|
text: '${order.workers}',
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
bg: UiColors.buttonSecondaryStill,
|
||||||
|
textColor: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
|
||||||
UiButton.secondary(
|
UiButton.secondary(
|
||||||
size: UiButtonSize.small,
|
size: UiButtonSize.small,
|
||||||
text: i18n.reorder_button,
|
text: i18n.reorder_button,
|
||||||
leadingIcon: UiIcons.zap,
|
leadingIcon: UiIcons.zap,
|
||||||
iconSize: 12,
|
iconSize: 12,
|
||||||
fullWidth: true,
|
fullWidth: true,
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
_handleReorderPressed(context, <String, dynamic>{
|
_handleReorderPressed(context, <String, dynamic>{
|
||||||
'orderId': order.orderId,
|
'orderId': order.orderId,
|
||||||
'title': order.title,
|
'title': order.title,
|
||||||
'location': order.location,
|
'location': order.location,
|
||||||
'hourlyRate': order.hourlyRate,
|
'hourlyRate': order.hourlyRate,
|
||||||
'hours': order.hours,
|
'hours': order.hours,
|
||||||
'workers': order.workers,
|
'workers': order.workers,
|
||||||
'type': order.type,
|
'type': order.type,
|
||||||
'totalCost': order.totalCost,
|
'totalCost': order.totalCost,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
|
||||||
|
/// Section header widget for home page sections, using design system tokens.
|
||||||
|
class SectionHeader extends StatelessWidget {
|
||||||
|
/// Section title
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// Optional subtitle
|
||||||
|
final String? subtitle;
|
||||||
|
|
||||||
|
/// Optional action label
|
||||||
|
final String? action;
|
||||||
|
|
||||||
|
/// Optional action callback
|
||||||
|
final VoidCallback? onAction;
|
||||||
|
|
||||||
|
/// Creates a [SectionHeader].
|
||||||
|
const SectionHeader({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
this.subtitle,
|
||||||
|
this.action,
|
||||||
|
this.onAction,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: action != null
|
||||||
|
? Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(title, style: UiTypography.body1b),
|
||||||
|
if (onAction != null)
|
||||||
|
GestureDetector(
|
||||||
|
onTap: onAction,
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
action ?? '',
|
||||||
|
style: UiTypography.body3r.textSecondary,
|
||||||
|
),
|
||||||
|
const Icon(
|
||||||
|
UiIcons.chevronRight,
|
||||||
|
size: UiConstants.space4,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: UiConstants.space2,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.primary.withValues(alpha: 0.08),
|
||||||
|
borderRadius: UiConstants.radiusMd,
|
||||||
|
border: Border.all(
|
||||||
|
color: UiColors.primary,
|
||||||
|
width: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
action!,
|
||||||
|
style: UiTypography.body3r.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Text(title, style: UiTypography.body1b),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (subtitle != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||||
|
child: Text(
|
||||||
|
subtitle!,
|
||||||
|
style: UiTypography.body2r.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'section_header.dart';
|
||||||
|
|
||||||
|
/// A common layout widget for home page sections.
|
||||||
|
///
|
||||||
|
/// Provides consistent structure with optional header and content area.
|
||||||
|
/// Use this to ensure all sections follow the same layout pattern.
|
||||||
|
class SectionLayout extends StatelessWidget {
|
||||||
|
|
||||||
|
/// Creates a [SectionLayout].
|
||||||
|
const SectionLayout({
|
||||||
|
this.title,
|
||||||
|
this.subtitle,
|
||||||
|
this.action,
|
||||||
|
this.onAction,
|
||||||
|
required this.child,
|
||||||
|
this.contentPadding,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
/// The title of the section, displayed in the header.
|
||||||
|
final String? title;
|
||||||
|
|
||||||
|
/// Optional subtitle for the section.
|
||||||
|
final String? subtitle;
|
||||||
|
|
||||||
|
/// Optional action text/widget to display on the right side of the header.
|
||||||
|
final String? action;
|
||||||
|
|
||||||
|
/// Optional callback when action is tapped.
|
||||||
|
final VoidCallback? onAction;
|
||||||
|
|
||||||
|
/// The main content of the section.
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
/// Optional padding for the content area.
|
||||||
|
/// Defaults to no padding.
|
||||||
|
final EdgeInsetsGeometry? contentPadding;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
if (title != null)
|
||||||
|
Padding(
|
||||||
|
padding: contentPadding ?? EdgeInsets.zero,
|
||||||
|
child: SectionHeader(
|
||||||
|
title: title!,
|
||||||
|
subtitle: subtitle,
|
||||||
|
action: action,
|
||||||
|
onAction: onAction,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
child,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import 'package:core_localization/core_localization.dart';
|
|||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'section_layout.dart';
|
||||||
|
|
||||||
/// A widget that displays spending insights for the client.
|
/// A widget that displays spending insights for the client.
|
||||||
class SpendingWidget extends StatelessWidget {
|
class SpendingWidget extends StatelessWidget {
|
||||||
|
|
||||||
@@ -12,6 +14,7 @@ class SpendingWidget extends StatelessWidget {
|
|||||||
required this.next7DaysSpending,
|
required this.next7DaysSpending,
|
||||||
required this.weeklyShifts,
|
required this.weeklyShifts,
|
||||||
required this.next7DaysScheduled,
|
required this.next7DaysScheduled,
|
||||||
|
this.title,
|
||||||
this.subtitle,
|
this.subtitle,
|
||||||
});
|
});
|
||||||
/// The spending this week.
|
/// The spending this week.
|
||||||
@@ -26,117 +29,103 @@ class SpendingWidget extends StatelessWidget {
|
|||||||
/// The number of scheduled shifts for next 7 days.
|
/// The number of scheduled shifts for next 7 days.
|
||||||
final int next7DaysScheduled;
|
final int next7DaysScheduled;
|
||||||
|
|
||||||
|
/// Optional title for the section.
|
||||||
|
final String? title;
|
||||||
|
|
||||||
/// Optional subtitle for the section.
|
/// Optional subtitle for the section.
|
||||||
final String? subtitle;
|
final String? subtitle;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final TranslationsClientHomeEn i18n = t.client_home;
|
return SectionLayout(
|
||||||
|
title: title,
|
||||||
return Column(
|
child: Container(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
padding: const EdgeInsets.all(UiConstants.space3),
|
||||||
children: <Widget>[
|
decoration: BoxDecoration(
|
||||||
Text(
|
gradient: LinearGradient(
|
||||||
i18n.widgets.spending.toUpperCase(),
|
colors: <Color>[
|
||||||
style: UiTypography.footnote1b.textSecondary.copyWith(
|
UiColors.primary,
|
||||||
letterSpacing: 0.5,
|
UiColors.primary.withValues(alpha: 0.85),
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
),
|
),
|
||||||
),
|
borderRadius: UiConstants.radiusLg,
|
||||||
if (subtitle != null) ...<Widget>[
|
boxShadow: <BoxShadow>[
|
||||||
Text(
|
BoxShadow(
|
||||||
subtitle!,
|
color: UiColors.primary.withValues(alpha: 0.3),
|
||||||
style: UiTypography.body2r.textSecondary,
|
blurRadius: 4,
|
||||||
),
|
offset: const Offset(0, 4),
|
||||||
],
|
|
||||||
const SizedBox(height: UiConstants.space6),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(UiConstants.space3),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: <Color>[
|
|
||||||
UiColors.primary,
|
|
||||||
UiColors.primary.withValues(alpha: 0.85),
|
|
||||||
],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
),
|
||||||
borderRadius: UiConstants.radiusLg,
|
],
|
||||||
boxShadow: <BoxShadow>[
|
|
||||||
BoxShadow(
|
|
||||||
color: UiColors.primary.withValues(alpha: 0.3),
|
|
||||||
blurRadius: 4,
|
|
||||||
offset: const Offset(0, 4),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: <Widget>[
|
|
||||||
Row(
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
t.client_home.dashboard.spending.this_week,
|
|
||||||
style: UiTypography.footnote2r.white.copyWith(
|
|
||||||
color: UiColors.white.withValues(alpha: 0.7),
|
|
||||||
fontSize: 9,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space1),
|
|
||||||
Text(
|
|
||||||
'\$${weeklySpending.toStringAsFixed(0)}',
|
|
||||||
style: UiTypography.headline3m.copyWith(
|
|
||||||
color: UiColors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
t.client_home.dashboard.spending.shifts_count(count: weeklyShifts),
|
|
||||||
style: UiTypography.footnote2r.white.copyWith(
|
|
||||||
color: UiColors.white.withValues(alpha: 0.6),
|
|
||||||
fontSize: 9,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
t.client_home.dashboard.spending.next_7_days,
|
|
||||||
style: UiTypography.footnote2r.white.copyWith(
|
|
||||||
color: UiColors.white.withValues(alpha: 0.7),
|
|
||||||
fontSize: 9,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space1),
|
|
||||||
Text(
|
|
||||||
'\$${next7DaysSpending.toStringAsFixed(0)}',
|
|
||||||
style: UiTypography.headline4m.copyWith(
|
|
||||||
color: UiColors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
t.client_home.dashboard.spending.scheduled_count(count: next7DaysScheduled),
|
|
||||||
style: UiTypography.footnote2r.white.copyWith(
|
|
||||||
color: UiColors.white.withValues(alpha: 0.6),
|
|
||||||
fontSize: 9,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
t.client_home.dashboard.spending.this_week,
|
||||||
|
style: UiTypography.footnote2r.white.copyWith(
|
||||||
|
color: UiColors.white.withValues(alpha: 0.7),
|
||||||
|
fontSize: 9,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
Text(
|
||||||
|
'\$${weeklySpending.toStringAsFixed(0)}',
|
||||||
|
style: UiTypography.headline3m.copyWith(
|
||||||
|
color: UiColors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
t.client_home.dashboard.spending.shifts_count(count: weeklyShifts),
|
||||||
|
style: UiTypography.footnote2r.white.copyWith(
|
||||||
|
color: UiColors.white.withValues(alpha: 0.6),
|
||||||
|
fontSize: 9,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
t.client_home.dashboard.spending.next_7_days,
|
||||||
|
style: UiTypography.footnote2r.white.copyWith(
|
||||||
|
color: UiColors.white.withValues(alpha: 0.7),
|
||||||
|
fontSize: 9,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
Text(
|
||||||
|
'\$${next7DaysSpending.toStringAsFixed(0)}',
|
||||||
|
style: UiTypography.headline4m.copyWith(
|
||||||
|
color: UiColors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
t.client_home.dashboard.spending.scheduled_count(count: next7DaysScheduled),
|
||||||
|
style: UiTypography.footnote2r.white.copyWith(
|
||||||
|
color: UiColors.white.withValues(alpha: 0.6),
|
||||||
|
fontSize: 9,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user