feat(shift-details): Refactor ShiftDetailsPage layout and implement new sections for breaks, date/time, description, and location
This commit is contained in:
@@ -10,7 +10,13 @@ import 'package:krow_domain/krow_domain.dart';
|
|||||||
import '../blocs/shift_details/shift_details_bloc.dart';
|
import '../blocs/shift_details/shift_details_bloc.dart';
|
||||||
import '../blocs/shift_details/shift_details_event.dart';
|
import '../blocs/shift_details/shift_details_event.dart';
|
||||||
import '../blocs/shift_details/shift_details_state.dart';
|
import '../blocs/shift_details/shift_details_state.dart';
|
||||||
import '../widgets/shift_location_map.dart';
|
import '../widgets/shift_details/shift_break_section.dart';
|
||||||
|
import '../widgets/shift_details/shift_date_time_section.dart';
|
||||||
|
import '../widgets/shift_details/shift_description_section.dart';
|
||||||
|
import '../widgets/shift_details/shift_details_bottom_bar.dart';
|
||||||
|
import '../widgets/shift_details/shift_details_header.dart';
|
||||||
|
import '../widgets/shift_details/shift_location_section.dart';
|
||||||
|
import '../widgets/shift_details/shift_stats_row.dart';
|
||||||
|
|
||||||
class ShiftDetailsPage extends StatefulWidget {
|
class ShiftDetailsPage extends StatefulWidget {
|
||||||
final String shiftId;
|
final String shiftId;
|
||||||
@@ -68,65 +74,6 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStatCard(IconData icon, String value, String label) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: UiConstants.space3),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: UiColors.bgThird,
|
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: UiColors.white,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: Icon(icon, size: 20, color: UiColors.textSecondary),
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space2),
|
|
||||||
Text(
|
|
||||||
value,
|
|
||||||
style: UiTypography.title1m
|
|
||||||
.copyWith(fontWeight: FontWeight.w700)
|
|
||||||
.textPrimary,
|
|
||||||
),
|
|
||||||
Text(label, style: UiTypography.footnote2r.textSecondary),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTimeBox(String label, String time) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(UiConstants.space3),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: UiColors.bgThird,
|
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style: UiTypography.footnote2b.copyWith(
|
|
||||||
color: UiColors.textSecondary,
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space1),
|
|
||||||
Text(
|
|
||||||
_formatTime(time),
|
|
||||||
style: UiTypography.title1m
|
|
||||||
.copyWith(fontWeight: FontWeight.w700)
|
|
||||||
.textPrimary,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<ShiftDetailsBloc>(
|
return BlocProvider<ShiftDetailsBloc>(
|
||||||
@@ -134,7 +81,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
..add(
|
..add(
|
||||||
LoadShiftDetailsEvent(widget.shiftId, roleId: widget.shift.roleId),
|
LoadShiftDetailsEvent(widget.shiftId, roleId: widget.shift.roleId),
|
||||||
),
|
),
|
||||||
child: BlocListener<ShiftDetailsBloc, ShiftDetailsState>(
|
child: BlocConsumer<ShiftDetailsBloc, ShiftDetailsState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state is ShiftActionSuccess || state is ShiftDetailsError) {
|
if (state is ShiftActionSuccess || state is ShiftDetailsError) {
|
||||||
_closeActionDialog(context);
|
_closeActionDialog(context);
|
||||||
@@ -158,345 +105,99 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
_isApplying = false;
|
_isApplying = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: BlocBuilder<ShiftDetailsBloc, ShiftDetailsState>(
|
builder: (context, state) {
|
||||||
builder: (context, state) {
|
if (state is ShiftDetailsLoading) {
|
||||||
if (state is ShiftDetailsLoading) {
|
return const Scaffold(
|
||||||
return const Scaffold(
|
body: Center(child: CircularProgressIndicator()),
|
||||||
body: Center(child: CircularProgressIndicator()),
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Shift? displayShift = widget.shift;
|
Shift displayShift = widget.shift;
|
||||||
|
final i18n = Translations.of(context).staff_shifts.shift_details;
|
||||||
|
|
||||||
final i18n = Translations.of(context).staff_shifts.shift_details;
|
final duration = _calculateDuration(displayShift);
|
||||||
|
final estimatedTotal =
|
||||||
|
displayShift.totalValue ?? (displayShift.hourlyRate * duration);
|
||||||
|
|
||||||
final duration = _calculateDuration(displayShift);
|
return Scaffold(
|
||||||
final estimatedTotal =
|
appBar: UiAppBar(
|
||||||
displayShift.totalValue ?? (displayShift.hourlyRate * duration);
|
centerTitle: false,
|
||||||
|
onLeadingPressed: () => Modular.to.toShifts(),
|
||||||
return Scaffold(
|
),
|
||||||
appBar: UiAppBar(
|
body: Column(
|
||||||
centerTitle: false,
|
children: [
|
||||||
onLeadingPressed: () => Modular.to.toShifts(),
|
Expanded(
|
||||||
),
|
child: SingleChildScrollView(
|
||||||
body: Column(
|
child: Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Expanded(
|
children: [
|
||||||
child: SingleChildScrollView(
|
ShiftDetailsHeader(shift: displayShift),
|
||||||
child: Column(
|
const Divider(height: 1, thickness: 0.5),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
ShiftStatsRow(
|
||||||
children: [
|
estimatedTotal: estimatedTotal,
|
||||||
// Role & Client Section
|
hourlyRate: displayShift.hourlyRate,
|
||||||
Padding(
|
duration: duration,
|
||||||
padding: const EdgeInsets.all(UiConstants.space5),
|
totalLabel: i18n.est_total,
|
||||||
child: Row(
|
hourlyRateLabel: i18n.hourly_rate,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
hoursLabel: i18n.hours,
|
||||||
spacing: UiConstants.space4,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 48,
|
|
||||||
height: 48,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: UiColors.background,
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
UiConstants.radiusBase,
|
|
||||||
),
|
|
||||||
border: Border.all(color: UiColors.border),
|
|
||||||
),
|
|
||||||
child: const Center(
|
|
||||||
child: Icon(
|
|
||||||
UiIcons.briefcase,
|
|
||||||
color: UiColors.primary,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment:
|
|
||||||
CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
displayShift.title,
|
|
||||||
style:
|
|
||||||
UiTypography.headline1b.textPrimary,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
displayShift.clientName,
|
|
||||||
style:
|
|
||||||
UiTypography.body1m.textSecondary,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
displayShift.locationAddress,
|
|
||||||
style:
|
|
||||||
UiTypography.body2r.textSecondary,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const Divider(height: 1, thickness: 0.5),
|
|
||||||
|
|
||||||
// Stats Row (New)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(UiConstants.space5),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: _buildStatCard(
|
|
||||||
UiIcons.dollar,
|
|
||||||
"\$${estimatedTotal.toStringAsFixed(0)}",
|
|
||||||
i18n.est_total,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space4),
|
|
||||||
Expanded(
|
|
||||||
child: _buildStatCard(
|
|
||||||
UiIcons.dollar,
|
|
||||||
"\$${displayShift.hourlyRate.toStringAsFixed(0)}",
|
|
||||||
i18n.hourly_rate,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space4),
|
|
||||||
Expanded(
|
|
||||||
child: _buildStatCard(
|
|
||||||
UiIcons.clock,
|
|
||||||
duration.toStringAsFixed(1),
|
|
||||||
i18n.hours,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const Divider(height: 1, thickness: 0.5),
|
|
||||||
|
|
||||||
// Date & Time Section
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(UiConstants.space5),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
i18n.shift_date,
|
|
||||||
style: UiTypography
|
|
||||||
.titleUppercase4b
|
|
||||||
.textSecondary,
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space2),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
UiIcons.calendar,
|
|
||||||
size: 20,
|
|
||||||
color: UiColors.primary,
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space2),
|
|
||||||
Text(
|
|
||||||
_formatDate(displayShift.date),
|
|
||||||
style:
|
|
||||||
UiTypography.headline5m.textPrimary,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space4),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: _buildTimeBox(
|
|
||||||
i18n.start_time,
|
|
||||||
displayShift.startTime,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space4),
|
|
||||||
Expanded(
|
|
||||||
child: _buildTimeBox(
|
|
||||||
i18n.end_time,
|
|
||||||
displayShift.endTime,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const Divider(height: 1, thickness: 0.5),
|
|
||||||
|
|
||||||
// Break Section
|
|
||||||
if (displayShift.breakInfo != null &&
|
|
||||||
displayShift.breakInfo!.duration !=
|
|
||||||
BreakDuration.none) ...[
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(UiConstants.space5),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
i18n.break_title,
|
|
||||||
style: UiTypography.titleUppercase4b
|
|
||||||
.textSecondary,
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space2),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
UiIcons.breakIcon,
|
|
||||||
size: 20,
|
|
||||||
color: UiColors.primary,
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space2),
|
|
||||||
Text(
|
|
||||||
"${displayShift.breakInfo!.duration.minutes} ${i18n.min} (${displayShift.breakInfo!.isBreakPaid ? i18n.paid : i18n.unpaid})",
|
|
||||||
style:
|
|
||||||
UiTypography.headline5m.textPrimary,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(height: 1, thickness: 0.5),
|
|
||||||
],
|
|
||||||
|
|
||||||
// Location Section (New with Map)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(UiConstants.space5),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
i18n.location,
|
|
||||||
style: UiTypography
|
|
||||||
.titleUppercase4b
|
|
||||||
.textSecondary,
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
displayShift.location.isEmpty
|
|
||||||
? i18n.tbd
|
|
||||||
: displayShift.location,
|
|
||||||
style: UiTypography.title1m.textPrimary,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space3),
|
|
||||||
OutlinedButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
ScaffoldMessenger.of(
|
|
||||||
context,
|
|
||||||
).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(
|
|
||||||
displayShift
|
|
||||||
.locationAddress
|
|
||||||
.isNotEmpty
|
|
||||||
? displayShift.locationAddress
|
|
||||||
: displayShift.location,
|
|
||||||
),
|
|
||||||
duration: const Duration(
|
|
||||||
seconds: 3,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: const Icon(
|
|
||||||
UiIcons.navigation,
|
|
||||||
size: UiConstants.iconXs,
|
|
||||||
),
|
|
||||||
label: Text(i18n.get_direction),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
foregroundColor: UiColors.textPrimary,
|
|
||||||
side: const BorderSide(
|
|
||||||
color: UiColors.border,
|
|
||||||
),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
UiConstants.radiusBase,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: UiConstants.space3,
|
|
||||||
vertical: 0,
|
|
||||||
),
|
|
||||||
minimumSize: const Size(0, 32),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space3),
|
|
||||||
ShiftLocationMap(
|
|
||||||
shift: displayShift,
|
|
||||||
height: 160,
|
|
||||||
borderRadius: UiConstants.radiusBase,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const Divider(height: 1, thickness: 0.5),
|
|
||||||
|
|
||||||
// Description / Instructions
|
|
||||||
if ((displayShift.description ?? '').isNotEmpty) ...[
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(UiConstants.space5),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
i18n.job_description,
|
|
||||||
style: UiTypography
|
|
||||||
.titleUppercase4b
|
|
||||||
.textSecondary,
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space2),
|
|
||||||
Text(
|
|
||||||
displayShift.description!,
|
|
||||||
style: UiTypography.body2r.textSecondary,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Bottom Action Bar
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.fromLTRB(
|
|
||||||
UiConstants.space5,
|
|
||||||
UiConstants.space4,
|
|
||||||
UiConstants.space5,
|
|
||||||
MediaQuery.of(context).padding.bottom +
|
|
||||||
UiConstants.space4,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: UiColors.white,
|
|
||||||
border: Border(top: BorderSide(color: UiColors.border)),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: UiColors.popupShadow.withValues(alpha: 0.05),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, -4),
|
|
||||||
),
|
),
|
||||||
|
const Divider(height: 1, thickness: 0.5),
|
||||||
|
ShiftDateTimeSection(
|
||||||
|
date: displayShift.date,
|
||||||
|
startTime: displayShift.startTime,
|
||||||
|
endTime: displayShift.endTime,
|
||||||
|
shiftDateLabel: i18n.shift_date,
|
||||||
|
clockInLabel: i18n.start_time,
|
||||||
|
clockOutLabel: i18n.end_time,
|
||||||
|
),
|
||||||
|
const Divider(height: 1, thickness: 0.5),
|
||||||
|
if (displayShift.breakInfo != null &&
|
||||||
|
displayShift.breakInfo!.duration !=
|
||||||
|
BreakDuration.none) ...[
|
||||||
|
ShiftBreakSection(
|
||||||
|
breakInfo: displayShift.breakInfo!,
|
||||||
|
breakTitle: i18n.break_title,
|
||||||
|
paidLabel: i18n.paid,
|
||||||
|
unpaidLabel: i18n.unpaid,
|
||||||
|
minLabel: i18n.min,
|
||||||
|
),
|
||||||
|
const Divider(height: 1, thickness: 0.5),
|
||||||
|
],
|
||||||
|
ShiftLocationSection(
|
||||||
|
shift: displayShift,
|
||||||
|
locationLabel: i18n.location,
|
||||||
|
tbdLabel: i18n.tbd,
|
||||||
|
getDirectionLabel: i18n.get_direction,
|
||||||
|
),
|
||||||
|
const Divider(height: 1, thickness: 0.5),
|
||||||
|
if (displayShift.description != null &&
|
||||||
|
displayShift.description!.isNotEmpty)
|
||||||
|
ShiftDescriptionSection(
|
||||||
|
description: displayShift.description!,
|
||||||
|
descriptionLabel: i18n.job_description,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: _buildBottomButton(displayShift, context),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
ShiftDetailsBottomBar(
|
||||||
);
|
shift: displayShift,
|
||||||
},
|
onApply: () => _bookShift(context, displayShift),
|
||||||
),
|
onDecline: () => BlocProvider.of<ShiftDetailsBloc>(
|
||||||
|
context,
|
||||||
|
).add(DeclineShiftDetailsEvent(displayShift.id)),
|
||||||
|
onAccept: () =>
|
||||||
|
BlocProvider.of<ShiftDetailsBloc>(context).add(
|
||||||
|
BookShiftDetailsEvent(
|
||||||
|
displayShift.id,
|
||||||
|
roleId: displayShift.roleId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -591,91 +292,4 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
Navigator.of(context, rootNavigator: true).pop();
|
Navigator.of(context, rootNavigator: true).pop();
|
||||||
_actionDialogOpen = false;
|
_actionDialogOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBottomButton(Shift shift, BuildContext context) {
|
|
||||||
final String status = shift.status ?? 'open';
|
|
||||||
|
|
||||||
final i18n = Translations.of(context).staff_shifts.shift_details;
|
|
||||||
if (status == 'confirmed') {
|
|
||||||
return SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: () => Modular.to.toClockIn(),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: UiColors.success,
|
|
||||||
foregroundColor: UiColors.white,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: UiConstants.space4),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
|
||||||
),
|
|
||||||
elevation: 0,
|
|
||||||
),
|
|
||||||
child: Text(i18n.clock_in, style: UiTypography.body2b.white),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status == 'pending') {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: OutlinedButton(
|
|
||||||
onPressed: () => BlocProvider.of<ShiftDetailsBloc>(
|
|
||||||
context,
|
|
||||||
).add(DeclineShiftDetailsEvent(shift.id)),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
foregroundColor: UiColors.destructive,
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
vertical: UiConstants.space4,
|
|
||||||
),
|
|
||||||
side: const BorderSide(color: UiColors.destructive),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(i18n.decline, style: UiTypography.body2b.textError),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space4),
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: () => BlocProvider.of<ShiftDetailsBloc>(
|
|
||||||
context,
|
|
||||||
).add(BookShiftDetailsEvent(shift.id, roleId: shift.roleId)),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: UiColors.primary,
|
|
||||||
foregroundColor: UiColors.white,
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
vertical: UiConstants.space4,
|
|
||||||
),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
|
||||||
),
|
|
||||||
elevation: 0,
|
|
||||||
),
|
|
||||||
child: Text(i18n.accept_shift, style: UiTypography.body2b.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status == 'open' || status == 'available') {
|
|
||||||
return ElevatedButton(
|
|
||||||
onPressed: () => _bookShift(context, shift),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: UiColors.primary,
|
|
||||||
foregroundColor: UiColors.white,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: UiConstants.space4),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
|
||||||
),
|
|
||||||
elevation: 0,
|
|
||||||
),
|
|
||||||
child: Text(i18n.apply_now, style: UiTypography.body2b.white),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return const SizedBox();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
/// A section displaying shift break details (duration and payment status).
|
||||||
|
class ShiftBreakSection extends StatelessWidget {
|
||||||
|
/// The break information.
|
||||||
|
final Break breakInfo;
|
||||||
|
|
||||||
|
/// Localization string for break section title.
|
||||||
|
final String breakTitle;
|
||||||
|
|
||||||
|
/// Localization string for paid status.
|
||||||
|
final String paidLabel;
|
||||||
|
|
||||||
|
/// Localization string for unpaid status.
|
||||||
|
final String unpaidLabel;
|
||||||
|
|
||||||
|
/// Localization string for minutes ("min").
|
||||||
|
final String minLabel;
|
||||||
|
|
||||||
|
/// Creates a [ShiftBreakSection].
|
||||||
|
const ShiftBreakSection({
|
||||||
|
super.key,
|
||||||
|
required this.breakInfo,
|
||||||
|
required this.breakTitle,
|
||||||
|
required this.paidLabel,
|
||||||
|
required this.unpaidLabel,
|
||||||
|
required this.minLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
breakTitle,
|
||||||
|
style: UiTypography.titleUppercase4b.textSecondary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
UiIcons.breakIcon,
|
||||||
|
size: 20,
|
||||||
|
color: UiColors.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
Text(
|
||||||
|
"${breakInfo.duration.minutes} $minLabel (${breakInfo.isBreakPaid ? paidLabel : unpaidLabel})",
|
||||||
|
style: UiTypography.headline5m.textPrimary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
/// A section displaying the date and the shift's start/end times.
|
||||||
|
class ShiftDateTimeSection extends StatelessWidget {
|
||||||
|
/// The ISO string of the date.
|
||||||
|
final String date;
|
||||||
|
|
||||||
|
/// The start time string (HH:mm).
|
||||||
|
final String startTime;
|
||||||
|
|
||||||
|
/// The end time string (HH:mm).
|
||||||
|
final String endTime;
|
||||||
|
|
||||||
|
/// Localization string for shift date.
|
||||||
|
final String shiftDateLabel;
|
||||||
|
|
||||||
|
/// Localization string for clock in time.
|
||||||
|
final String clockInLabel;
|
||||||
|
|
||||||
|
/// Localization string for clock out time.
|
||||||
|
final String clockOutLabel;
|
||||||
|
|
||||||
|
/// Creates a [ShiftDateTimeSection].
|
||||||
|
const ShiftDateTimeSection({
|
||||||
|
super.key,
|
||||||
|
required this.date,
|
||||||
|
required this.startTime,
|
||||||
|
required this.endTime,
|
||||||
|
required this.shiftDateLabel,
|
||||||
|
required this.clockInLabel,
|
||||||
|
required this.clockOutLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
String _formatTime(String time) {
|
||||||
|
if (time.isEmpty) return '';
|
||||||
|
try {
|
||||||
|
final parts = time.split(':');
|
||||||
|
final hour = int.parse(parts[0]);
|
||||||
|
final minute = int.parse(parts[1]);
|
||||||
|
final dt = DateTime(2022, 1, 1, hour, minute);
|
||||||
|
return DateFormat('h:mm a').format(dt);
|
||||||
|
} catch (e) {
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatDate(String dateStr) {
|
||||||
|
if (dateStr.isEmpty) return '';
|
||||||
|
try {
|
||||||
|
final date = DateTime.parse(dateStr);
|
||||||
|
return DateFormat('EEEE, MMMM d, y').format(date);
|
||||||
|
} catch (e) {
|
||||||
|
return dateStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
shiftDateLabel,
|
||||||
|
style: UiTypography.titleUppercase4b.textSecondary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
UiIcons.calendar,
|
||||||
|
size: 20,
|
||||||
|
color: UiColors.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
Text(
|
||||||
|
_formatDate(date),
|
||||||
|
style: UiTypography.headline5m.textPrimary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildTimeBox(
|
||||||
|
clockInLabel,
|
||||||
|
startTime,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space4),
|
||||||
|
Expanded(
|
||||||
|
child: _buildTimeBox(
|
||||||
|
clockOutLabel,
|
||||||
|
endTime,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTimeBox(String label, String time) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.bgThird,
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: UiTypography.footnote2b.copyWith(
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
Text(
|
||||||
|
_formatTime(time),
|
||||||
|
style: UiTypography.title1m
|
||||||
|
.copyWith(fontWeight: FontWeight.w700)
|
||||||
|
.textPrimary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A section displaying the job description for the shift.
|
||||||
|
class ShiftDescriptionSection extends StatelessWidget {
|
||||||
|
/// The description text.
|
||||||
|
final String description;
|
||||||
|
|
||||||
|
/// Localization string for description section title.
|
||||||
|
final String descriptionLabel;
|
||||||
|
|
||||||
|
/// Creates a [ShiftDescriptionSection].
|
||||||
|
const ShiftDescriptionSection({
|
||||||
|
super.key,
|
||||||
|
required this.description,
|
||||||
|
required this.descriptionLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (description.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
descriptionLabel,
|
||||||
|
style: UiTypography.titleUppercase4b.textSecondary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
Text(
|
||||||
|
description,
|
||||||
|
style: UiTypography.body2r.textSecondary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
|
/// A bottom action bar containing contextual buttons based on shift status.
|
||||||
|
class ShiftDetailsBottomBar extends StatelessWidget {
|
||||||
|
/// The current shift.
|
||||||
|
final Shift shift;
|
||||||
|
|
||||||
|
/// Callback for applying/booking a shift.
|
||||||
|
final VoidCallback onApply;
|
||||||
|
|
||||||
|
/// Callback for declining a shift.
|
||||||
|
final VoidCallback onDecline;
|
||||||
|
|
||||||
|
/// Callback for accepting a shift.
|
||||||
|
final VoidCallback onAccept;
|
||||||
|
|
||||||
|
/// Creates a [ShiftDetailsBottomBar].
|
||||||
|
const ShiftDetailsBottomBar({
|
||||||
|
super.key,
|
||||||
|
required this.shift,
|
||||||
|
required this.onApply,
|
||||||
|
required this.onDecline,
|
||||||
|
required this.onAccept,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final String status = shift.status ?? 'open';
|
||||||
|
final i18n = Translations.of(context).staff_shifts.shift_details;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
UiConstants.space5,
|
||||||
|
UiConstants.space4,
|
||||||
|
UiConstants.space5,
|
||||||
|
MediaQuery.of(context).padding.bottom + UiConstants.space4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
border: Border(top: BorderSide(color: UiColors.border)),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: UiColors.popupShadow.withValues(alpha: 0.05),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, -4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: _buildButtons(status, i18n, context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildButtons(String status, dynamic i18n, BuildContext context) {
|
||||||
|
if (status == 'confirmed') {
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => Modular.to.toClockIn(),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: UiColors.success,
|
||||||
|
foregroundColor: UiColors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: UiConstants.space4),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: Text(i18n.clock_in, style: UiTypography.body2b.white),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status == 'pending') {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: onDecline,
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: UiColors.destructive,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: UiConstants.space4,
|
||||||
|
),
|
||||||
|
side: const BorderSide(color: UiColors.destructive),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(i18n.decline, style: UiTypography.body2b.textError),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space4),
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: onAccept,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: UiColors.primary,
|
||||||
|
foregroundColor: UiColors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: UiConstants.space4,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: Text(i18n.accept_shift, style: UiTypography.body2b.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status == 'open' || status == 'available') {
|
||||||
|
return ElevatedButton(
|
||||||
|
onPressed: onApply,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: UiColors.primary,
|
||||||
|
foregroundColor: UiColors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: UiConstants.space4),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: Text(i18n.apply_now, style: UiTypography.body2b.white),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
/// A header widget for the shift details page displaying the role, client name, and address.
|
||||||
|
class ShiftDetailsHeader extends StatelessWidget {
|
||||||
|
/// The shift entity containing the header information.
|
||||||
|
final Shift shift;
|
||||||
|
|
||||||
|
/// Creates a [ShiftDetailsHeader].
|
||||||
|
const ShiftDetailsHeader({
|
||||||
|
super.key,
|
||||||
|
required this.shift,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: UiConstants.space4,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.background,
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
UiConstants.radiusBase,
|
||||||
|
),
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(
|
||||||
|
UiIcons.briefcase,
|
||||||
|
color: UiColors.primary,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
shift.title,
|
||||||
|
style: UiTypography.headline1b.textPrimary,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
shift.clientName,
|
||||||
|
style: UiTypography.body1m.textSecondary,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
shift.locationAddress,
|
||||||
|
style: UiTypography.body2r.textSecondary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import '../shift_location_map.dart';
|
||||||
|
|
||||||
|
/// A section displaying the shift's location, address, map, and "Get direction" action.
|
||||||
|
class ShiftLocationSection extends StatelessWidget {
|
||||||
|
/// The shift entity containing location data.
|
||||||
|
final Shift shift;
|
||||||
|
|
||||||
|
/// Localization string for location section title.
|
||||||
|
final String locationLabel;
|
||||||
|
|
||||||
|
/// Localization string for "TBD".
|
||||||
|
final String tbdLabel;
|
||||||
|
|
||||||
|
/// Localization string for "Get direction".
|
||||||
|
final String getDirectionLabel;
|
||||||
|
|
||||||
|
/// Creates a [ShiftLocationSection].
|
||||||
|
const ShiftLocationSection({
|
||||||
|
super.key,
|
||||||
|
required this.shift,
|
||||||
|
required this.locationLabel,
|
||||||
|
required this.tbdLabel,
|
||||||
|
required this.getDirectionLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
locationLabel,
|
||||||
|
style: UiTypography.titleUppercase4b.textSecondary,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
shift.location.isEmpty ? tbdLabel : shift.location,
|
||||||
|
style: UiTypography.title1m.textPrimary,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space3),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
shift.locationAddress.isNotEmpty
|
||||||
|
? shift.locationAddress
|
||||||
|
: shift.location,
|
||||||
|
),
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
UiIcons.navigation,
|
||||||
|
size: UiConstants.iconXs,
|
||||||
|
),
|
||||||
|
label: Text(getDirectionLabel),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: UiColors.textPrimary,
|
||||||
|
side: const BorderSide(color: UiColors.border),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: UiConstants.space3,
|
||||||
|
vertical: 0,
|
||||||
|
),
|
||||||
|
minimumSize: const Size(0, 32),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
ShiftLocationMap(
|
||||||
|
shift: shift,
|
||||||
|
height: 160,
|
||||||
|
borderRadius: UiConstants.radiusBase,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A row of statistic cards for shift details (Total Pay, Rate, Hours).
|
||||||
|
class ShiftStatsRow extends StatelessWidget {
|
||||||
|
/// Estimated total pay for the shift.
|
||||||
|
final double estimatedTotal;
|
||||||
|
|
||||||
|
/// Hourly rate for the shift.
|
||||||
|
final double hourlyRate;
|
||||||
|
|
||||||
|
/// Total duration of the shift in hours.
|
||||||
|
final double duration;
|
||||||
|
|
||||||
|
/// Localization string for total.
|
||||||
|
final String totalLabel;
|
||||||
|
|
||||||
|
/// Localization string for hourly rate.
|
||||||
|
final String hourlyRateLabel;
|
||||||
|
|
||||||
|
/// Localization string for hours.
|
||||||
|
final String hoursLabel;
|
||||||
|
|
||||||
|
/// Creates a [ShiftStatsRow].
|
||||||
|
const ShiftStatsRow({
|
||||||
|
super.key,
|
||||||
|
required this.estimatedTotal,
|
||||||
|
required this.hourlyRate,
|
||||||
|
required this.duration,
|
||||||
|
required this.totalLabel,
|
||||||
|
required this.hourlyRateLabel,
|
||||||
|
required this.hoursLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildStatCard(
|
||||||
|
UiIcons.dollar,
|
||||||
|
"\$${estimatedTotal.toStringAsFixed(0)}",
|
||||||
|
totalLabel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space4),
|
||||||
|
Expanded(
|
||||||
|
child: _buildStatCard(
|
||||||
|
UiIcons.dollar,
|
||||||
|
"\$${hourlyRate.toStringAsFixed(0)}",
|
||||||
|
hourlyRateLabel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space4),
|
||||||
|
Expanded(
|
||||||
|
child: _buildStatCard(
|
||||||
|
UiIcons.clock,
|
||||||
|
duration.toStringAsFixed(1),
|
||||||
|
hoursLabel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatCard(IconData icon, String value, String label) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: UiConstants.space3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.bgThird,
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(icon, size: 20, color: UiColors.textSecondary),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: UiTypography.title1m
|
||||||
|
.copyWith(fontWeight: FontWeight.w700)
|
||||||
|
.textPrimary,
|
||||||
|
),
|
||||||
|
Text(label, style: UiTypography.footnote2r.textSecondary),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user