diff --git a/apps/.gitignore b/apps/.gitignore new file mode 100644 index 00000000..9bbffcff --- /dev/null +++ b/apps/.gitignore @@ -0,0 +1,19 @@ +# Old apps prototypes +prototypes/* + +# AI prompts +ai_prompts/* + +# Template feature +packages/features/shared/template_feature/* + +# Generated files +*.g.dart +*.freezed.dart + +# Dart/Flutter +.dart_tool/ +.packages +.pub-cache/ +.pub/ +build/ diff --git a/apps/packages/features/client/home/lib/src/presentation/pages/client_home_page.dart b/apps/packages/features/client/home/lib/src/presentation/pages/client_home_page.dart index 3633f59f..4a0c00ae 100644 --- a/apps/packages/features/client/home/lib/src/presentation/pages/client_home_page.dart +++ b/apps/packages/features/client/home/lib/src/presentation/pages/client_home_page.dart @@ -8,6 +8,7 @@ import '../blocs/client_home_bloc.dart'; import '../blocs/client_home_event.dart'; import '../blocs/client_home_state.dart'; import '../widgets/actions_widget.dart'; +import '../widgets/coverage_widget.dart'; import '../widgets/live_activity_widget.dart'; import '../widgets/reorder_widget.dart'; import '../widgets/shift_order_form_sheet.dart'; @@ -341,6 +342,7 @@ class _ClientHomePageState extends State { next7DaysScheduled: state.next7DaysScheduled, ); case 'coverage': + return const CoverageWidget(); case 'liveActivity': return LiveActivityWidget(onViewAllPressed: () {}); default: diff --git a/apps/packages/features/client/home/lib/src/presentation/widgets/coverage_widget.dart b/apps/packages/features/client/home/lib/src/presentation/widgets/coverage_widget.dart new file mode 100644 index 00000000..fea90fb1 --- /dev/null +++ b/apps/packages/features/client/home/lib/src/presentation/widgets/coverage_widget.dart @@ -0,0 +1,152 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A widget that displays the daily coverage metrics. +class CoverageWidget extends StatelessWidget { + /// The total number of shifts needed. + final int totalNeeded; + + /// The number of confirmed shifts. + final int totalConfirmed; + + /// The percentage of coverage (0-100). + final int coveragePercent; + + /// Creates a [CoverageWidget]. + const CoverageWidget({ + super.key, + this.totalNeeded = 10, + this.totalConfirmed = 8, + this.coveragePercent = 80, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "TODAY'S COVERAGE", + style: UiTypography.footnote1b.copyWith( + color: UiColors.textPrimary, + letterSpacing: 0.5, + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space2, + vertical: + 2, // 2px is not in metrics, using hardcoded for small tweaks or space0/space1 + ), + decoration: BoxDecoration( + color: UiColors.tagActive, + borderRadius: UiConstants.radiusLg, + ), + child: Text( + '$coveragePercent% Covered', + style: UiTypography.footnote2b.copyWith( + color: UiColors.textSuccess, + ), + ), + ), + ], + ), + const SizedBox(height: UiConstants.space2), + Row( + children: [ + Expanded( + child: _MetricCard( + icon: UiIcons.target, + iconColor: UiColors.primary, + label: 'Needed', + value: '$totalNeeded', + ), + ), + const SizedBox(width: UiConstants.space2), + Expanded( + child: _MetricCard( + icon: UiIcons.success, + iconColor: UiColors.iconSuccess, + label: 'Filled', + value: '$totalConfirmed', + valueColor: UiColors.textSuccess, + ), + ), + const SizedBox(width: UiConstants.space2), + Expanded( + child: _MetricCard( + icon: UiIcons.error, + iconColor: UiColors.iconError, + label: 'Open', + value: '${totalNeeded - totalConfirmed}', + valueColor: UiColors.textError, + ), + ), + ], + ), + ], + ); + } +} + +class _MetricCard extends StatelessWidget { + final IconData icon; + final Color iconColor; + final String label; + final String value; + final Color? valueColor; + + const _MetricCard({ + required this.icon, + required this.iconColor, + required this.label, + required this.value, + this.valueColor, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space2 + 2), // 10px + decoration: BoxDecoration( + color: UiColors.cardViewBackground, + borderRadius: UiConstants.radiusLg, + border: Border.all(color: UiColors.border), + boxShadow: [ + BoxShadow( + color: UiColors.black.withValues(alpha: 0.02), + blurRadius: 2, + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, size: 14, color: iconColor), + const SizedBox(width: 6), // 6px + Text( + label, + style: UiTypography.footnote2m.copyWith( + color: UiColors.textSecondary, + ), + ), + ], + ), + const SizedBox(width: 6), // 6px + Text( + value, + style: UiTypography.headline3m.copyWith( + color: valueColor ?? UiColors.textPrimary, + fontWeight: + FontWeight.bold, // header3 is usually bold, but ensuring + ), + ), + ], + ), + ); + } +}