feat(client_coverage): add client coverage feature with user session data use case and dashboard widgets

- Created `client_coverage` feature with necessary dependencies in `pubspec.yaml`.
- Implemented `GetUserSessionDataUseCase` for retrieving user session data.
- Developed `ClientHomeEditBanner` for edit mode instructions and reset functionality.
- Added `ClientHomeHeader` to display user information and action buttons.
- Built `DashboardWidgetBuilder` to render various dashboard widgets based on state.
- Introduced `DraggableWidgetWrapper` for managing widget visibility and drag handles in edit mode.
- Created `HeaderIconButton` for interactive header actions with optional badge support.
This commit is contained in:
Achintha Isuru
2026-01-23 16:25:01 -05:00
parent 597028436d
commit 2b331e356b
39 changed files with 3032 additions and 426 deletions

View File

@@ -0,0 +1,2 @@
export 'src/coverage_module.dart';
export 'src/presentation/pages/coverage_page.dart';

View File

@@ -0,0 +1,33 @@
import 'package:flutter_modular/flutter_modular.dart';
import 'data/repositories_impl/coverage_repository_impl.dart';
import 'domain/repositories/coverage_repository.dart';
import 'domain/usecases/get_coverage_stats_usecase.dart';
import 'domain/usecases/get_shifts_for_date_usecase.dart';
import 'presentation/blocs/coverage_bloc.dart';
import 'presentation/pages/coverage_page.dart';
/// Modular module for the coverage feature.
class CoverageModule extends Module {
@override
void binds(Injector i) {
// Repositories
i.addSingleton<CoverageRepository>(CoverageRepositoryImpl.new);
// Use Cases
i.addSingleton(GetShiftsForDateUseCase.new);
i.addSingleton(GetCoverageStatsUseCase.new);
// BLoCs
i.addSingleton<CoverageBloc>(
() => CoverageBloc(
getShiftsForDate: i.get<GetShiftsForDateUseCase>(),
getCoverageStats: i.get<GetCoverageStatsUseCase>(),
),
);
}
@override
void routes(RouteManager r) {
r.child('/', child: (_) => const CoveragePage());
}
}

View File

@@ -0,0 +1,123 @@
import '../../domain/repositories/coverage_repository.dart';
import '../../domain/ui_entities/coverage_entities.dart';
/// Implementation of [CoverageRepository] in the Data layer.
///
/// This class provides mock data for the coverage feature.
/// In a production environment, this would delegate to `packages/data_connect`
/// for real data access (e.g., Firebase Data Connect, REST API).
///
/// It strictly adheres to the Clean Architecture data layer responsibilities:
/// - No business logic (except necessary data transformation).
/// - Delegates to data sources (currently mock data, will be `data_connect`).
/// - Returns domain entities from `domain/ui_entities`.
class CoverageRepositoryImpl implements CoverageRepository {
/// Creates a [CoverageRepositoryImpl].
CoverageRepositoryImpl();
/// Fetches shifts for a specific date.
@override
Future<List<CoverageShift>> getShiftsForDate({required DateTime date}) async {
// Simulate network delay
await Future<void>.delayed(const Duration(milliseconds: 500));
// Mock data - in production, this would come from data_connect
final DateTime today = DateTime.now();
final bool isToday = date.year == today.year &&
date.month == today.month &&
date.day == today.day;
if (!isToday) {
// Return empty list for non-today dates
return <CoverageShift>[];
}
return <CoverageShift>[
CoverageShift(
id: '1',
title: 'Banquet Server',
location: 'Grand Ballroom',
startTime: '16:00',
workersNeeded: 10,
date: date,
workers: const <CoverageWorker>[
CoverageWorker(
name: 'Sarah Wilson',
status: 'confirmed',
checkInTime: '15:55',
),
CoverageWorker(
name: 'Mike Ross',
status: 'confirmed',
checkInTime: '16:00',
),
CoverageWorker(
name: 'Jane Doe',
status: 'confirmed',
checkInTime: null,
),
CoverageWorker(
name: 'John Smith',
status: 'late',
checkInTime: null,
),
],
),
CoverageShift(
id: '2',
title: 'Bartender',
location: 'Lobby Bar',
startTime: '17:00',
workersNeeded: 4,
date: date,
workers: const <CoverageWorker>[
CoverageWorker(
name: 'Emily Blunt',
status: 'confirmed',
checkInTime: '16:45',
),
CoverageWorker(
name: 'Chris Evans',
status: 'confirmed',
checkInTime: '16:50',
),
CoverageWorker(
name: 'Tom Holland',
status: 'confirmed',
checkInTime: null,
),
],
),
];
}
/// Fetches coverage statistics for a specific date.
@override
Future<CoverageStats> getCoverageStats({required DateTime date}) async {
// Get shifts for the date
final List<CoverageShift> shifts = await getShiftsForDate(date: date);
// Calculate statistics
final int totalNeeded = shifts.fold<int>(
0,
(int sum, CoverageShift shift) => sum + shift.workersNeeded,
);
final List<CoverageWorker> allWorkers =
shifts.expand((CoverageShift shift) => shift.workers).toList();
final int totalConfirmed = allWorkers.length;
final int checkedIn =
allWorkers.where((CoverageWorker w) => w.isCheckedIn).length;
final int enRoute =
allWorkers.where((CoverageWorker w) => w.isEnRoute).length;
final int late = allWorkers.where((CoverageWorker w) => w.isLate).length;
return CoverageStats(
totalNeeded: totalNeeded,
totalConfirmed: totalConfirmed,
checkedIn: checkedIn,
enRoute: enRoute,
late: late,
);
}
}

View File

@@ -0,0 +1,16 @@
import 'package:krow_core/core.dart';
/// Arguments for fetching coverage statistics for a specific date.
///
/// This argument class encapsulates the date parameter required by
/// the [GetCoverageStatsUseCase].
class GetCoverageStatsArguments extends UseCaseArgument {
/// Creates [GetCoverageStatsArguments].
const GetCoverageStatsArguments({required this.date});
/// The date to fetch coverage statistics for.
final DateTime date;
@override
List<Object?> get props => <Object?>[date];
}

View File

@@ -0,0 +1,16 @@
import 'package:krow_core/core.dart';
/// Arguments for fetching shifts for a specific date.
///
/// This argument class encapsulates the date parameter required by
/// the [GetShiftsForDateUseCase].
class GetShiftsForDateArguments extends UseCaseArgument {
/// Creates [GetShiftsForDateArguments].
const GetShiftsForDateArguments({required this.date});
/// The date to fetch shifts for.
final DateTime date;
@override
List<Object?> get props => <Object?>[date];
}

View File

@@ -0,0 +1,23 @@
import '../ui_entities/coverage_entities.dart';
/// Repository interface for coverage-related operations.
///
/// This interface defines the contract for accessing coverage data,
/// acting as a boundary between the Domain and Data layers.
/// It allows the Domain layer to remain independent of specific data sources.
///
/// Implementation of this interface must delegate all data access through
/// the `packages/data_connect` layer, ensuring compliance with Clean Architecture.
abstract interface class CoverageRepository {
/// Fetches shifts for a specific date.
///
/// Returns a list of [CoverageShift] entities representing all shifts
/// scheduled for the given [date].
Future<List<CoverageShift>> getShiftsForDate({required DateTime date});
/// Fetches coverage statistics for a specific date.
///
/// Returns [CoverageStats] containing aggregated metrics including
/// total workers needed, confirmed, checked in, en route, and late.
Future<CoverageStats> getCoverageStats({required DateTime date});
}

View File

@@ -0,0 +1,133 @@
import 'package:equatable/equatable.dart';
/// Domain entity representing a shift in the coverage view.
///
/// This is a feature-specific domain entity that encapsulates shift information
/// including scheduling details and assigned workers.
class CoverageShift extends Equatable {
/// Creates a [CoverageShift].
const CoverageShift({
required this.id,
required this.title,
required this.location,
required this.startTime,
required this.workersNeeded,
required this.date,
required this.workers,
});
/// The unique identifier for the shift.
final String id;
/// The title or role of the shift.
final String title;
/// The location where the shift takes place.
final String location;
/// The start time of the shift (e.g., "16:00").
final String startTime;
/// The number of workers needed for this shift.
final int workersNeeded;
/// The date of the shift.
final DateTime date;
/// The list of workers assigned to this shift.
final List<CoverageWorker> workers;
/// Calculates the coverage percentage for this shift.
int get coveragePercent {
if (workersNeeded == 0) return 100;
return ((workers.length / workersNeeded) * 100).round();
}
@override
List<Object?> get props => <Object?>[
id,
title,
location,
startTime,
workersNeeded,
date,
workers,
];
}
/// Domain entity representing a worker in the coverage view.
///
/// This entity tracks worker status including check-in information.
class CoverageWorker extends Equatable {
/// Creates a [CoverageWorker].
const CoverageWorker({
required this.name,
required this.status,
this.checkInTime,
});
/// The name of the worker.
final String name;
/// The status of the worker ('confirmed', 'late', etc.).
final String status;
/// The time the worker checked in, if applicable.
final String? checkInTime;
/// Returns true if the worker is checked in.
bool get isCheckedIn => status == 'confirmed' && checkInTime != null;
/// Returns true if the worker is en route.
bool get isEnRoute => status == 'confirmed' && checkInTime == null;
/// Returns true if the worker is late.
bool get isLate => status == 'late';
@override
List<Object?> get props => <Object?>[name, status, checkInTime];
}
/// Domain entity representing coverage statistics.
///
/// Aggregates coverage metrics for a specific date.
class CoverageStats extends Equatable {
/// Creates a [CoverageStats].
const CoverageStats({
required this.totalNeeded,
required this.totalConfirmed,
required this.checkedIn,
required this.enRoute,
required this.late,
});
/// The total number of workers needed.
final int totalNeeded;
/// The total number of confirmed workers.
final int totalConfirmed;
/// The number of workers who have checked in.
final int checkedIn;
/// The number of workers en route.
final int enRoute;
/// The number of late workers.
final int late;
/// Calculates the overall coverage percentage.
int get coveragePercent {
if (totalNeeded == 0) return 100;
return ((totalConfirmed / totalNeeded) * 100).round();
}
@override
List<Object?> get props => <Object?>[
totalNeeded,
totalConfirmed,
checkedIn,
enRoute,
late,
];
}

View File

@@ -0,0 +1,28 @@
import 'package:krow_core/core.dart';
import '../arguments/get_coverage_stats_arguments.dart';
import '../repositories/coverage_repository.dart';
import '../ui_entities/coverage_entities.dart';
/// Use case for fetching coverage statistics for a specific date.
///
/// This use case encapsulates the logic for retrieving coverage metrics including
/// total workers needed, confirmed, checked in, en route, and late.
/// It delegates the data retrieval to the [CoverageRepository].
///
/// Follows the KROW Clean Architecture pattern by:
/// - Extending from [UseCase] base class
/// - Using [GetCoverageStatsArguments] for input
/// - Returning domain entities ([CoverageStats])
/// - Delegating to repository abstraction
class GetCoverageStatsUseCase
implements UseCase<GetCoverageStatsArguments, CoverageStats> {
/// Creates a [GetCoverageStatsUseCase].
GetCoverageStatsUseCase(this._repository);
final CoverageRepository _repository;
@override
Future<CoverageStats> call(GetCoverageStatsArguments arguments) {
return _repository.getCoverageStats(date: arguments.date);
}
}

View File

@@ -0,0 +1,27 @@
import 'package:krow_core/core.dart';
import '../arguments/get_shifts_for_date_arguments.dart';
import '../repositories/coverage_repository.dart';
import '../ui_entities/coverage_entities.dart';
/// Use case for fetching shifts for a specific date.
///
/// This use case encapsulates the logic for retrieving all shifts scheduled for a given date.
/// It delegates the data retrieval to the [CoverageRepository].
///
/// Follows the KROW Clean Architecture pattern by:
/// - Extending from [UseCase] base class
/// - Using [GetShiftsForDateArguments] for input
/// - Returning domain entities ([CoverageShift])
/// - Delegating to repository abstraction
class GetShiftsForDateUseCase
implements UseCase<GetShiftsForDateArguments, List<CoverageShift>> {
/// Creates a [GetShiftsForDateUseCase].
GetShiftsForDateUseCase(this._repository);
final CoverageRepository _repository;
@override
Future<List<CoverageShift>> call(GetShiftsForDateArguments arguments) {
return _repository.getShiftsForDate(date: arguments.date);
}
}

View File

@@ -0,0 +1,80 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/arguments/get_coverage_stats_arguments.dart';
import '../../domain/arguments/get_shifts_for_date_arguments.dart';
import '../../domain/ui_entities/coverage_entities.dart';
import '../../domain/usecases/get_coverage_stats_usecase.dart';
import '../../domain/usecases/get_shifts_for_date_usecase.dart';
import 'coverage_event.dart';
import 'coverage_state.dart';
/// BLoC for managing coverage feature state.
///
/// This BLoC handles:
/// - Loading shifts for a specific date
/// - Loading coverage statistics
/// - Refreshing coverage data
class CoverageBloc extends Bloc<CoverageEvent, CoverageState> {
/// Creates a [CoverageBloc].
CoverageBloc({
required GetShiftsForDateUseCase getShiftsForDate,
required GetCoverageStatsUseCase getCoverageStats,
}) : _getShiftsForDate = getShiftsForDate,
_getCoverageStats = getCoverageStats,
super(const CoverageState()) {
on<CoverageLoadRequested>(_onLoadRequested);
on<CoverageRefreshRequested>(_onRefreshRequested);
}
final GetShiftsForDateUseCase _getShiftsForDate;
final GetCoverageStatsUseCase _getCoverageStats;
/// Handles the load requested event.
Future<void> _onLoadRequested(
CoverageLoadRequested event,
Emitter<CoverageState> emit,
) async {
emit(
state.copyWith(
status: CoverageStatus.loading,
selectedDate: event.date,
),
);
try {
// Fetch shifts and stats concurrently
final List<Object> results = await Future.wait<Object>(<Future<Object>>[
_getShiftsForDate(GetShiftsForDateArguments(date: event.date)),
_getCoverageStats(GetCoverageStatsArguments(date: event.date)),
]);
final List<CoverageShift> shifts = results[0] as List<CoverageShift>;
final CoverageStats stats = results[1] as CoverageStats;
emit(
state.copyWith(
status: CoverageStatus.success,
shifts: shifts,
stats: stats,
),
);
} catch (error) {
emit(
state.copyWith(
status: CoverageStatus.failure,
errorMessage: error.toString(),
),
);
}
}
/// Handles the refresh requested event.
Future<void> _onRefreshRequested(
CoverageRefreshRequested event,
Emitter<CoverageState> emit,
) async {
if (state.selectedDate == null) return;
// Reload data for the current selected date
add(CoverageLoadRequested(date: state.selectedDate!));
}
}

View File

@@ -0,0 +1,28 @@
import 'package:equatable/equatable.dart';
/// Base class for all coverage events.
sealed class CoverageEvent extends Equatable {
/// Creates a [CoverageEvent].
const CoverageEvent();
@override
List<Object?> get props => <Object?>[];
}
/// Event to load coverage data for a specific date.
final class CoverageLoadRequested extends CoverageEvent {
/// Creates a [CoverageLoadRequested] event.
const CoverageLoadRequested({required this.date});
/// The date to load coverage data for.
final DateTime date;
@override
List<Object?> get props => <Object?>[date];
}
/// Event to refresh coverage data.
final class CoverageRefreshRequested extends CoverageEvent {
/// Creates a [CoverageRefreshRequested] event.
const CoverageRefreshRequested();
}

View File

@@ -0,0 +1,70 @@
import 'package:equatable/equatable.dart';
import '../../domain/ui_entities/coverage_entities.dart';
/// Enum representing the status of coverage data loading.
enum CoverageStatus {
/// Initial state before any data is loaded.
initial,
/// Data is currently being loaded.
loading,
/// Data has been successfully loaded.
success,
/// An error occurred while loading data.
failure,
}
/// State for the coverage feature.
final class CoverageState extends Equatable {
/// Creates a [CoverageState].
const CoverageState({
this.status = CoverageStatus.initial,
this.selectedDate,
this.shifts = const <CoverageShift>[],
this.stats,
this.errorMessage,
});
/// The current status of data loading.
final CoverageStatus status;
/// The currently selected date.
final DateTime? selectedDate;
/// The list of shifts for the selected date.
final List<CoverageShift> shifts;
/// Coverage statistics for the selected date.
final CoverageStats? stats;
/// Error message if status is failure.
final String? errorMessage;
/// Creates a copy of this state with the given fields replaced.
CoverageState copyWith({
CoverageStatus? status,
DateTime? selectedDate,
List<CoverageShift>? shifts,
CoverageStats? stats,
String? errorMessage,
}) {
return CoverageState(
status: status ?? this.status,
selectedDate: selectedDate ?? this.selectedDate,
shifts: shifts ?? this.shifts,
stats: stats ?? this.stats,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => <Object?>[
status,
selectedDate,
shifts,
stats,
errorMessage,
];
}

View File

@@ -0,0 +1,128 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import '../blocs/coverage_bloc.dart';
import '../blocs/coverage_event.dart';
import '../blocs/coverage_state.dart';
import '../widgets/coverage_header.dart';
import '../widgets/coverage_quick_stats.dart';
import '../widgets/coverage_shift_list.dart';
import '../widgets/late_workers_alert.dart';
/// Page for displaying daily coverage information.
///
/// Shows shifts, worker statuses, and coverage statistics for a selected date.
class CoveragePage extends StatelessWidget {
/// Creates a [CoveragePage].
const CoveragePage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider<CoverageBloc>(
create: (BuildContext context) => Modular.get<CoverageBloc>()
..add(CoverageLoadRequested(date: DateTime.now())),
child: Scaffold(
backgroundColor: UiColors.background,
body: BlocBuilder<CoverageBloc, CoverageState>(
builder: (BuildContext context, CoverageState state) {
return Column(
children: <Widget>[
CoverageHeader(
selectedDate: state.selectedDate ?? DateTime.now(),
coveragePercent: state.stats?.coveragePercent ?? 0,
totalConfirmed: state.stats?.totalConfirmed ?? 0,
totalNeeded: state.stats?.totalNeeded ?? 0,
onDateSelected: (DateTime date) {
BlocProvider.of<CoverageBloc>(context).add(
CoverageLoadRequested(date: date),
);
},
onRefresh: () {
BlocProvider.of<CoverageBloc>(context).add(
const CoverageRefreshRequested(),
);
},
),
Expanded(
child: _buildBody(context: context, state: state),
),
],
);
},
),
),
);
}
/// Builds the main body content based on the current state.
Widget _buildBody({
required BuildContext context,
required CoverageState state,
}) {
if (state.status == CoverageStatus.loading) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (state.status == CoverageStatus.failure) {
return Center(
child: Padding(
padding: const EdgeInsets.all(UiConstants.space6),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Icon(
UiIcons.warning,
size: UiConstants.space12,
color: UiColors.destructive,
),
const SizedBox(height: UiConstants.space4),
Text(
'Failed to load coverage data',
style: UiTypography.title2m.copyWith(
color: UiColors.textPrimary,
),
),
const SizedBox(height: UiConstants.space2),
Text(
state.errorMessage ?? 'An unknown error occurred',
style: UiTypography.body2r.copyWith(
color: UiColors.mutedForeground,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.all(UiConstants.space5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
if (state.stats != null) ...<Widget>[
CoverageQuickStats(stats: state.stats!),
const SizedBox(height: UiConstants.space5),
],
if (state.stats != null && state.stats!.late > 0) ...<Widget>[
LateWorkersAlert(lateCount: state.stats!.late),
const SizedBox(height: UiConstants.space5),
],
Text(
'Shifts',
style: UiTypography.title2b.copyWith(
color: UiColors.textPrimary,
),
),
const SizedBox(height: UiConstants.space3),
CoverageShiftList(shifts: state.shifts),
],
),
);
}
}

View File

@@ -0,0 +1,185 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
/// Calendar selector widget for choosing dates.
///
/// Displays a week view with navigation buttons and date selection.
class CoverageCalendarSelector extends StatefulWidget {
/// Creates a [CoverageCalendarSelector].
const CoverageCalendarSelector({
required this.selectedDate,
required this.onDateSelected,
super.key,
});
/// The currently selected date.
final DateTime selectedDate;
/// Callback when a date is selected.
final ValueChanged<DateTime> onDateSelected;
@override
State<CoverageCalendarSelector> createState() =>
_CoverageCalendarSelectorState();
}
class _CoverageCalendarSelectorState extends State<CoverageCalendarSelector> {
late DateTime _today;
@override
void initState() {
super.initState();
_today = DateTime.now();
_today = DateTime(_today.year, _today.month, _today.day);
}
/// Gets the list of calendar days to display (7 days centered on selected date).
List<DateTime> _getCalendarDays() {
final List<DateTime> days = <DateTime>[];
final DateTime startDate =
widget.selectedDate.subtract(const Duration(days: 3));
for (int i = 0; i < 7; i++) {
days.add(startDate.add(Duration(days: i)));
}
return days;
}
/// Navigates to the previous week.
void _navigatePrevWeek() {
widget.onDateSelected(
widget.selectedDate.subtract(const Duration(days: 7)),
);
}
/// Navigates to today's date.
void _navigateToday() {
final DateTime now = DateTime.now();
widget.onDateSelected(DateTime(now.year, now.month, now.day));
}
/// Navigates to the next week.
void _navigateNextWeek() {
widget.onDateSelected(
widget.selectedDate.add(const Duration(days: 7)),
);
}
@override
Widget build(BuildContext context) {
final List<DateTime> calendarDays = _getCalendarDays();
return Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
_NavButton(
text: '← Prev Week',
onTap: _navigatePrevWeek,
),
_NavButton(
text: 'Today',
onTap: _navigateToday,
),
_NavButton(
text: 'Next Week →',
onTap: _navigateNextWeek,
),
],
),
const SizedBox(height: UiConstants.space2),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: calendarDays.map((DateTime date) {
final bool isSelected = date.year == widget.selectedDate.year &&
date.month == widget.selectedDate.month &&
date.day == widget.selectedDate.day;
final bool isToday = date.year == _today.year &&
date.month == _today.month &&
date.day == _today.day;
return GestureDetector(
onTap: () => widget.onDateSelected(date),
child: Container(
width: UiConstants.space10 + UiConstants.space1,
height: UiConstants.space14,
decoration: BoxDecoration(
color: isSelected
? UiColors.primaryForeground
: UiColors.primaryForeground.withOpacity(0.1),
borderRadius: UiConstants.radiusLg,
border: isToday && !isSelected
? Border.all(
color: UiColors.primaryForeground,
width: 2,
)
: null,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
date.day.toString().padLeft(2, '0'),
style: UiTypography.body1b.copyWith(
color: isSelected
? UiColors.primary
: UiColors.primaryForeground,
),
),
Text(
DateFormat('E').format(date),
style: UiTypography.body4m.copyWith(
color: isSelected
? UiColors.mutedForeground
: UiColors.primaryForeground.withOpacity(0.7),
),
),
],
),
),
);
}).toList(),
),
],
);
}
}
/// Navigation button for calendar navigation.
class _NavButton extends StatelessWidget {
/// Creates a [_NavButton].
const _NavButton({
required this.text,
required this.onTap,
});
/// The button text.
final String text;
/// Callback when tapped.
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space3,
vertical: UiConstants.space1,
),
decoration: BoxDecoration(
color: UiColors.primaryForeground.withOpacity(0.2),
borderRadius: UiConstants.radiusMd,
),
child: Text(
text,
style: UiTypography.body3r.copyWith(
color: UiColors.primaryForeground,
),
),
),
);
}
}

View File

@@ -0,0 +1,176 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'coverage_calendar_selector.dart';
/// Header widget for the coverage page.
///
/// Displays:
/// - Back button and title
/// - Refresh button
/// - Calendar date selector
/// - Coverage summary statistics
class CoverageHeader extends StatelessWidget {
/// Creates a [CoverageHeader].
const CoverageHeader({
required this.selectedDate,
required this.coveragePercent,
required this.totalConfirmed,
required this.totalNeeded,
required this.onDateSelected,
required this.onRefresh,
super.key,
});
/// The currently selected date.
final DateTime selectedDate;
/// The coverage percentage.
final int coveragePercent;
/// The total number of confirmed workers.
final int totalConfirmed;
/// The total number of workers needed.
final int totalNeeded;
/// Callback when a date is selected.
final ValueChanged<DateTime> onDateSelected;
/// Callback when refresh is requested.
final VoidCallback onRefresh;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.only(
top: UiConstants.space14,
left: UiConstants.space5,
right: UiConstants.space5,
bottom: UiConstants.space6,
),
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: <Color>[
UiColors.primary,
UiColors.accent,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: <Widget>[
GestureDetector(
onTap: () => Modular.to.pop(),
child: Container(
width: UiConstants.space10,
height: UiConstants.space10,
decoration: BoxDecoration(
color: UiColors.primaryForeground.withOpacity(0.2),
shape: BoxShape.circle,
),
child: const Icon(
UiIcons.arrowLeft,
color: UiColors.primaryForeground,
size: UiConstants.space5,
),
),
),
const SizedBox(width: UiConstants.space3),
Text(
'Daily Coverage',
style: UiTypography.title1m.copyWith(
color: UiColors.primaryForeground,
),
),
],
),
Container(
width: UiConstants.space8,
height: UiConstants.space8,
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: UiConstants.radiusMd,
),
child: IconButton(
onPressed: onRefresh,
icon: const Icon(
UiIcons.rotateCcw,
color: UiColors.primaryForeground,
size: UiConstants.space4,
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
style: IconButton.styleFrom(
hoverColor: UiColors.primaryForeground.withOpacity(0.2),
shape: RoundedRectangleBorder(
borderRadius: UiConstants.radiusMd,
),
),
),
),
],
),
const SizedBox(height: UiConstants.space4),
CoverageCalendarSelector(
selectedDate: selectedDate,
onDateSelected: onDateSelected,
),
const SizedBox(height: UiConstants.space4),
Container(
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
color: UiColors.primaryForeground.withOpacity(0.1),
borderRadius: UiConstants.radiusLg,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Coverage Status',
style: UiTypography.body2r.copyWith(
color: UiColors.primaryForeground.withOpacity(0.7),
),
),
Text(
'$coveragePercent%',
style: UiTypography.display1b.copyWith(
color: UiColors.primaryForeground,
),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Text(
'Workers',
style: UiTypography.body2r.copyWith(
color: UiColors.primaryForeground.withOpacity(0.7),
),
),
Text(
'$totalConfirmed/$totalNeeded',
style: UiTypography.title2m.copyWith(
color: UiColors.primaryForeground,
),
),
],
),
],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,112 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import '../../domain/ui_entities/coverage_entities.dart';
/// Quick statistics cards showing coverage metrics.
///
/// Displays checked-in, en-route, and late worker counts.
class CoverageQuickStats extends StatelessWidget {
/// Creates a [CoverageQuickStats].
const CoverageQuickStats({
required this.stats,
super.key,
});
/// The coverage statistics to display.
final CoverageStats stats;
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(
child: _StatCard(
icon: UiIcons.success,
label: 'Checked In',
value: stats.checkedIn.toString(),
color: UiColors.iconSuccess,
),
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: _StatCard(
icon: UiIcons.clock,
label: 'En Route',
value: stats.enRoute.toString(),
color: UiColors.textWarning,
),
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: _StatCard(
icon: UiIcons.warning,
label: 'Late',
value: stats.late.toString(),
color: UiColors.destructive,
),
),
],
);
}
}
/// Individual stat card widget.
class _StatCard extends StatelessWidget {
/// Creates a [_StatCard].
const _StatCard({
required this.icon,
required this.label,
required this.value,
required this.color,
});
/// The icon to display.
final IconData icon;
/// The label text.
final String label;
/// The value to display.
final String value;
/// The accent color for the card.
final Color color;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(UiConstants.space3),
decoration: BoxDecoration(
color: UiColors.bgMenu,
borderRadius: UiConstants.radiusLg,
border: Border.all(
color: UiColors.border,
),
),
child: Column(
children: <Widget>[
Icon(
icon,
color: color,
size: UiConstants.space6,
),
const SizedBox(height: UiConstants.space2),
Text(
value,
style: UiTypography.title1m.copyWith(
color: UiColors.textPrimary,
),
),
const SizedBox(height: UiConstants.space1),
Text(
label,
style: UiTypography.body3r.copyWith(
color: UiColors.mutedForeground,
),
textAlign: TextAlign.center,
),
],
),
);
}
}

View File

@@ -0,0 +1,433 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../domain/ui_entities/coverage_entities.dart';
/// List of shifts with their workers.
///
/// Displays all shifts for the selected date, or an empty state if none exist.
class CoverageShiftList extends StatelessWidget {
/// Creates a [CoverageShiftList].
const CoverageShiftList({
required this.shifts,
super.key,
});
/// The list of shifts to display.
final List<CoverageShift> shifts;
/// Formats a time string (HH:mm) to a readable format (h:mm a).
String _formatTime(String? time) {
if (time == null) return '';
final List<String> parts = time.split(':');
final DateTime dt = DateTime(
2022,
1,
1,
int.parse(parts[0]),
int.parse(parts[1]),
);
return DateFormat('h:mm a').format(dt);
}
@override
Widget build(BuildContext context) {
if (shifts.isEmpty) {
return Container(
padding: const EdgeInsets.all(UiConstants.space8),
decoration: BoxDecoration(
color: UiColors.bgPopup,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border),
),
child: Column(
children: <Widget>[
const Icon(
UiIcons.users,
size: UiConstants.space12,
color: UiColors.mutedForeground,
),
const SizedBox(height: UiConstants.space3),
Text(
'No shifts scheduled for this day',
style: UiTypography.body2r.copyWith(
color: UiColors.mutedForeground,
),
),
],
),
);
}
return Column(
children: shifts.map((CoverageShift shift) {
return Container(
margin: const EdgeInsets.only(bottom: UiConstants.space3),
decoration: BoxDecoration(
color: UiColors.bgPopup,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border),
),
clipBehavior: Clip.antiAlias,
child: Column(
children: <Widget>[
_ShiftHeader(
title: shift.title,
location: shift.location,
startTime: _formatTime(shift.startTime),
current: shift.workers.length,
total: shift.workersNeeded,
coveragePercent: shift.coveragePercent,
),
if (shift.workers.isNotEmpty)
Padding(
padding: const EdgeInsets.all(UiConstants.space3),
child: Column(
children:
shift.workers.map<Widget>((CoverageWorker worker) {
final bool isLast = worker == shift.workers.last;
return Padding(
padding: EdgeInsets.only(
bottom: isLast ? 0 : UiConstants.space2,
),
child: _WorkerRow(
worker: worker,
shiftStartTime: _formatTime(shift.startTime),
formatTime: _formatTime,
),
);
}).toList(),
),
)
else
Padding(
padding: const EdgeInsets.all(UiConstants.space4),
child: Text(
'No workers assigned yet',
style: UiTypography.body3r.copyWith(
color: UiColors.mutedForeground,
),
),
),
],
),
);
}).toList(),
);
}
}
/// Header for a shift card.
class _ShiftHeader extends StatelessWidget {
/// Creates a [_ShiftHeader].
const _ShiftHeader({
required this.title,
required this.location,
required this.startTime,
required this.current,
required this.total,
required this.coveragePercent,
});
/// The shift title.
final String title;
/// The shift location.
final String location;
/// The shift start time.
final String startTime;
/// Current number of workers.
final int current;
/// Total workers needed.
final int total;
/// Coverage percentage.
final int coveragePercent;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(UiConstants.space4),
decoration: const BoxDecoration(
color: UiColors.muted,
border: Border(
bottom: BorderSide(
color: UiColors.border,
),
),
),
child: Row(
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
Container(
width: UiConstants.space2,
height: UiConstants.space2,
decoration: const BoxDecoration(
color: UiColors.primary,
shape: BoxShape.circle,
),
),
const SizedBox(width: UiConstants.space2),
Text(
title,
style: UiTypography.body1b.copyWith(
color: UiColors.textPrimary,
),
),
],
),
const SizedBox(height: UiConstants.space2),
Row(
children: <Widget>[
const Icon(
UiIcons.mapPin,
size: UiConstants.space3,
color: UiColors.mutedForeground,
),
const SizedBox(width: UiConstants.space1),
Text(
location,
style: UiTypography.body3r.copyWith(
color: UiColors.mutedForeground,
),
),
const SizedBox(width: UiConstants.space3),
const Icon(
UiIcons.clock,
size: UiConstants.space3,
color: UiColors.mutedForeground,
),
const SizedBox(width: UiConstants.space1),
Text(
startTime,
style: UiTypography.body3r.copyWith(
color: UiColors.mutedForeground,
),
),
],
),
],
),
),
_CoverageBadge(
current: current,
total: total,
coveragePercent: coveragePercent,
),
],
),
);
}
}
/// Coverage badge showing worker count and status.
class _CoverageBadge extends StatelessWidget {
/// Creates a [_CoverageBadge].
const _CoverageBadge({
required this.current,
required this.total,
required this.coveragePercent,
});
/// Current number of workers.
final int current;
/// Total workers needed.
final int total;
/// Coverage percentage.
final int coveragePercent;
@override
Widget build(BuildContext context) {
Color bg;
Color text;
if (coveragePercent >= 100) {
bg = UiColors.textSuccess;
text = UiColors.primaryForeground;
} else if (coveragePercent >= 80) {
bg = UiColors.textWarning;
text = UiColors.primaryForeground;
} else {
bg = UiColors.destructive;
text = UiColors.destructiveForeground;
}
return Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space2 + UiConstants.space1,
vertical: UiConstants.space1 / 2,
),
decoration: BoxDecoration(
color: bg,
borderRadius: UiConstants.radiusFull,
),
child: Text(
'$current/$total',
style: UiTypography.body3m.copyWith(
color: text,
),
),
);
}
}
/// Row displaying a single worker's status.
class _WorkerRow extends StatelessWidget {
/// Creates a [_WorkerRow].
const _WorkerRow({
required this.worker,
required this.shiftStartTime,
required this.formatTime,
});
/// The worker to display.
final CoverageWorker worker;
/// The shift start time.
final String shiftStartTime;
/// Function to format time strings.
final String Function(String?) formatTime;
@override
Widget build(BuildContext context) {
Color bg;
Color border;
Color textBg;
Color textColor;
IconData icon;
String statusText;
Color badgeBg;
Color badgeText;
String badgeLabel;
if (worker.isCheckedIn) {
bg = UiColors.textSuccess.withOpacity(0.1);
border = UiColors.textSuccess;
textBg = UiColors.textSuccess.withOpacity(0.2);
textColor = UiColors.textSuccess;
icon = UiIcons.success;
statusText = '✓ Checked In at ${formatTime(worker.checkInTime)}';
badgeBg = UiColors.textSuccess;
badgeText = UiColors.primaryForeground;
badgeLabel = 'On Site';
} else if (worker.isEnRoute) {
bg = UiColors.textWarning.withOpacity(0.1);
border = UiColors.textWarning;
textBg = UiColors.textWarning.withOpacity(0.2);
textColor = UiColors.textWarning;
icon = UiIcons.clock;
statusText = 'En Route - Expected $shiftStartTime';
badgeBg = UiColors.textWarning;
badgeText = UiColors.primaryForeground;
badgeLabel = 'En Route';
} else {
bg = UiColors.destructive.withOpacity(0.1);
border = UiColors.destructive;
textBg = UiColors.destructive.withOpacity(0.2);
textColor = UiColors.destructive;
icon = UiIcons.warning;
statusText = '⚠ Running Late';
badgeBg = UiColors.destructive;
badgeText = UiColors.destructiveForeground;
badgeLabel = 'Late';
}
return Container(
padding: const EdgeInsets.all(UiConstants.space2),
decoration: BoxDecoration(
color: bg,
borderRadius: UiConstants.radiusMd,
),
child: Row(
children: <Widget>[
Stack(
clipBehavior: Clip.none,
children: <Widget>[
Container(
width: UiConstants.space10,
height: UiConstants.space10,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: border, width: 2),
),
child: CircleAvatar(
backgroundColor: textBg,
child: Text(
worker.name.isNotEmpty ? worker.name[0] : 'W',
style: UiTypography.body1b.copyWith(
color: textColor,
),
),
),
),
Positioned(
bottom: -2,
right: -2,
child: Container(
width: UiConstants.space4,
height: UiConstants.space4,
decoration: BoxDecoration(
color: border,
shape: BoxShape.circle,
),
child: Icon(
icon,
size: UiConstants.space2 + UiConstants.space1,
color: UiColors.primaryForeground,
),
),
),
],
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
worker.name,
style: UiTypography.body2b.copyWith(
color: UiColors.textPrimary,
),
),
Text(
statusText,
style: UiTypography.body3m.copyWith(
color: textColor,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space2,
vertical: UiConstants.space1 / 2,
),
decoration: BoxDecoration(
color: badgeBg,
borderRadius: UiConstants.radiusFull,
),
child: Text(
badgeLabel,
style: UiTypography.footnote2b.copyWith(
color: badgeText,
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,60 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Alert widget for displaying late workers warning.
///
/// Shows a warning banner when there are late workers.
class LateWorkersAlert extends StatelessWidget {
/// Creates a [LateWorkersAlert].
const LateWorkersAlert({
required this.lateCount,
super.key,
});
/// The number of late workers.
final int lateCount;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(UiConstants.space3),
decoration: BoxDecoration(
color: UiColors.destructive.withOpacity(0.1),
borderRadius: UiConstants.radiusLg,
border: Border.all(
color: UiColors.destructive.withOpacity(0.3),
),
),
child: Row(
children: <Widget>[
const Icon(
UiIcons.warning,
color: UiColors.destructive,
size: UiConstants.space5,
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Late Workers Alert',
style: UiTypography.body1b.copyWith(
color: UiColors.destructive,
),
),
const SizedBox(height: UiConstants.space1),
Text(
'$lateCount ${lateCount == 1 ? 'worker is' : 'workers are'} running late',
style: UiTypography.body3r.copyWith(
color: UiColors.destructiveForeground,
),
),
],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,650 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev"
source: hosted
version: "2.13.0"
auto_injector:
dependency: transitive
description:
name: auto_injector
sha256: "1fc2624898e92485122eb2b1698dd42511d7ff6574f84a3a8606fc4549a1e8f8"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
bloc:
dependency: transitive
description:
name: bloc
sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e"
url: "https://pub.dev"
source: hosted
version: "8.1.4"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.4.0"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
core_localization:
dependency: "direct main"
description:
path: "../../../core_localization"
relative: true
source: path
version: "0.0.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
csv:
dependency: transitive
description:
name: csv
sha256: c6aa2679b2a18cb57652920f674488d89712efaf4d3fdf2e537215b35fc19d6c
url: "https://pub.dev"
source: hosted
version: "6.0.0"
design_system:
dependency: "direct main"
description:
path: "../../../design_system"
relative: true
source: path
version: "0.0.1"
equatable:
dependency: "direct main"
description:
name: equatable
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
url: "https://pub.dev"
source: hosted
version: "2.0.8"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c
url: "https://pub.dev"
source: hosted
version: "2.1.5"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_bloc:
dependency: "direct main"
description:
name: flutter_bloc
sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a
url: "https://pub.dev"
source: hosted
version: "8.1.6"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_localizations:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
flutter_modular:
dependency: "direct main"
description:
name: flutter_modular
sha256: "33a63d9fe61429d12b3dfa04795ed890f17d179d3d38e988ba7969651fcd5586"
url: "https://pub.dev"
source: hosted
version: "6.4.1"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
font_awesome_flutter:
dependency: transitive
description:
name: font_awesome_flutter
sha256: b9011df3a1fa02993630b8fb83526368cf2206a711259830325bab2f1d2a4eb0
url: "https://pub.dev"
source: hosted
version: "10.12.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
google_fonts:
dependency: transitive
description:
name: google_fonts
sha256: "6996212014b996eaa17074e02b1b925b212f5e053832d9048970dc27255a8fb3"
url: "https://pub.dev"
source: hosted
version: "7.1.0"
hooks:
dependency: transitive
description:
name: hooks
sha256: "5d309c86e7ce34cd8e37aa71cb30cb652d3829b900ab145e4d9da564b31d59f7"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
http:
dependency: transitive
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
intl:
dependency: "direct main"
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.20.2"
krow_core:
dependency: "direct main"
description:
path: "../../../core"
relative: true
source: path
version: "0.0.1"
krow_data_connect:
dependency: "direct main"
description:
path: "../../../data_connect"
relative: true
source: path
version: "0.0.1"
krow_domain:
dependency: "direct main"
description:
path: "../../../domain"
relative: true
source: path
version: "0.0.1"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
url: "https://pub.dev"
source: hosted
version: "5.1.1"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
lucide_icons:
dependency: transitive
description:
name: lucide_icons
sha256: ad24d0fd65707e48add30bebada7d90bff2a1bba0a72d6e9b19d44246b0e83c4
url: "https://pub.dev"
source: hosted
version: "0.257.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
modular_core:
dependency: transitive
description:
name: modular_core
sha256: "1db0420a0dfb8a2c6dca846e7cbaa4ffeb778e247916dbcb27fb25aa566e5436"
url: "https://pub.dev"
source: hosted
version: "3.4.1"
native_toolchain_c:
dependency: transitive
description:
name: native_toolchain_c
sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac"
url: "https://pub.dev"
source: hosted
version: "0.17.4"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "7fd0c4d8ac8980011753b9bdaed2bf15111365924cdeeeaeb596214ea2b03537"
url: "https://pub.dev"
source: hosted
version: "9.2.4"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
url: "https://pub.dev"
source: hosted
version: "2.2.22"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
provider:
dependency: transitive
description:
name: provider
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
result_dart:
dependency: transitive
description:
name: result_dart
sha256: "0666b21fbdf697b3bdd9986348a380aa204b3ebe7c146d8e4cdaa7ce735e6054"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
shared_preferences:
dependency: transitive
description:
name: shared_preferences
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc"
url: "https://pub.dev"
source: hosted
version: "2.4.18"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
url: "https://pub.dev"
source: hosted
version: "2.5.6"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
slang:
dependency: transitive
description:
name: slang
sha256: "13e3b6f07adc51ab751e7889647774d294cbce7a3382f81d9e5029acfe9c37b2"
url: "https://pub.dev"
source: hosted
version: "4.12.0"
slang_flutter:
dependency: transitive
description:
name: slang_flutter
sha256: "0a4545cca5404d6b7487cf61cf1fe56c52daeb08de56a7574ee8381fbad035a0"
url: "https://pub.dev"
source: hosted
version: "4.12.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev"
source: hosted
version: "0.7.7"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
uuid:
dependency: transitive
description:
name: uuid
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
url: "https://pub.dev"
source: hosted
version: "4.5.2"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
url: "https://pub.dev"
source: hosted
version: "15.0.2"
watcher:
dependency: transitive
description:
name: watcher
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.10.7 <4.0.0"
flutter: ">=3.38.4"

View File

@@ -0,0 +1,34 @@
name: client_coverage
description: Client coverage feature for tracking daily shift coverage and worker status
version: 1.0.0
publish_to: none
environment:
sdk: ^3.6.0
dependencies:
flutter:
sdk: flutter
# Internal packages
design_system:
path: ../../../design_system
krow_domain:
path: ../../../domain
krow_core:
path: ../../../core
krow_data_connect:
path: ../../../data_connect
core_localization:
path: ../../../core_localization
# External packages
flutter_modular: ^6.3.4
flutter_bloc: ^8.1.6
equatable: ^2.0.7
intl: ^0.20.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0