feat: Implement cross-platform NFC clocking interface with calendar and shift sections
This commit is contained in:
@@ -0,0 +1,44 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A section header with a colored dot indicator and title text.
|
||||||
|
class SectionHeader extends StatelessWidget {
|
||||||
|
/// Creates a [SectionHeader].
|
||||||
|
const SectionHeader({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.dotColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The header title text.
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// The color of the leading dot indicator.
|
||||||
|
final Color dotColor;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: UiConstants.space4),
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: dotColor,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: dotColor == UiColors.textSecondary
|
||||||
|
? UiTypography.body2b.textSecondary
|
||||||
|
: UiTypography.body2b.copyWith(color: dotColor),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart' show ReadContext;
|
||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
import 'package:staff_shifts/src/presentation/blocs/shifts/shifts_bloc.dart';
|
||||||
|
import 'package:staff_shifts/src/presentation/widgets/shared/empty_state_view.dart';
|
||||||
|
import 'package:staff_shifts/src/presentation/widgets/shift_card/index.dart';
|
||||||
|
import 'section_header.dart';
|
||||||
|
|
||||||
|
/// Scrollable list displaying pending, cancelled, and confirmed shift sections.
|
||||||
|
///
|
||||||
|
/// Renders each section with a [SectionHeader] and a list of [ShiftCard]
|
||||||
|
/// widgets. Shows an [EmptyStateView] when all sections are empty.
|
||||||
|
class ShiftSectionList extends StatelessWidget {
|
||||||
|
/// Creates a [ShiftSectionList].
|
||||||
|
const ShiftSectionList({
|
||||||
|
super.key,
|
||||||
|
required this.assignedShifts,
|
||||||
|
required this.pendingAssignments,
|
||||||
|
required this.cancelledShifts,
|
||||||
|
this.submittedShiftIds = const <String>{},
|
||||||
|
this.submittingShiftId,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Confirmed/assigned shifts visible for the selected day.
|
||||||
|
final List<AssignedShift> assignedShifts;
|
||||||
|
|
||||||
|
/// Pending assignments awaiting acceptance.
|
||||||
|
final List<PendingAssignment> pendingAssignments;
|
||||||
|
|
||||||
|
/// Cancelled shifts visible for the selected week.
|
||||||
|
final List<CancelledShift> cancelledShifts;
|
||||||
|
|
||||||
|
/// Set of shift IDs that have been successfully submitted for approval.
|
||||||
|
final Set<String> submittedShiftIds;
|
||||||
|
|
||||||
|
/// The shift ID currently being submitted (null when idle).
|
||||||
|
final String? submittingShiftId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: UiConstants.space5,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
const SizedBox(height: UiConstants.space5),
|
||||||
|
|
||||||
|
// Pending assignments section
|
||||||
|
if (pendingAssignments.isNotEmpty) ...<Widget>[
|
||||||
|
SectionHeader(
|
||||||
|
title:
|
||||||
|
context.t.staff_shifts.my_shifts_tab.sections.awaiting,
|
||||||
|
dotColor: UiColors.textWarning,
|
||||||
|
),
|
||||||
|
...pendingAssignments.map(
|
||||||
|
(PendingAssignment assignment) => Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
bottom: UiConstants.space4,
|
||||||
|
),
|
||||||
|
child: ShiftCard(
|
||||||
|
data: ShiftCardData.fromPending(assignment),
|
||||||
|
onTap: () =>
|
||||||
|
Modular.to.toShiftDetailsById(assignment.shiftId),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Cancelled shifts section
|
||||||
|
if (cancelledShifts.isNotEmpty) ...<Widget>[
|
||||||
|
SectionHeader(
|
||||||
|
title:
|
||||||
|
context.t.staff_shifts.my_shifts_tab.sections.cancelled,
|
||||||
|
dotColor: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
...cancelledShifts.map(
|
||||||
|
(CancelledShift cs) => Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
bottom: UiConstants.space4,
|
||||||
|
),
|
||||||
|
child: ShiftCard(
|
||||||
|
data: ShiftCardData.fromCancelled(cs),
|
||||||
|
onTap: () =>
|
||||||
|
Modular.to.toShiftDetailsById(cs.shiftId),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Confirmed shifts section
|
||||||
|
if (assignedShifts.isNotEmpty) ...<Widget>[
|
||||||
|
SectionHeader(
|
||||||
|
title:
|
||||||
|
context.t.staff_shifts.my_shifts_tab.sections.confirmed,
|
||||||
|
dotColor: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
...assignedShifts.map(
|
||||||
|
(AssignedShift shift) {
|
||||||
|
final bool isCompleted =
|
||||||
|
shift.status == AssignmentStatus.completed;
|
||||||
|
final bool isSubmitted =
|
||||||
|
submittedShiftIds.contains(shift.shiftId);
|
||||||
|
final bool isSubmitting =
|
||||||
|
submittingShiftId == shift.shiftId;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
bottom: UiConstants.space3,
|
||||||
|
),
|
||||||
|
child: ShiftCard(
|
||||||
|
data: ShiftCardData.fromAssigned(shift),
|
||||||
|
onTap: () =>
|
||||||
|
Modular.to.toShiftDetailsById(shift.shiftId),
|
||||||
|
showApprovalAction: isCompleted,
|
||||||
|
isSubmitted: isSubmitted,
|
||||||
|
isSubmitting: isSubmitting,
|
||||||
|
onSubmitForApproval: () {
|
||||||
|
ReadContext(context).read<ShiftsBloc>().add(
|
||||||
|
SubmitForApprovalEvent(
|
||||||
|
shiftId: shift.shiftId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
UiSnackbar.show(
|
||||||
|
context,
|
||||||
|
message: context.t.staff_shifts
|
||||||
|
.my_shift_card.timesheet_submitted,
|
||||||
|
type: UiSnackbarType.success,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Empty state
|
||||||
|
if (assignedShifts.isEmpty &&
|
||||||
|
pendingAssignments.isEmpty &&
|
||||||
|
cancelledShifts.isEmpty)
|
||||||
|
EmptyStateView(
|
||||||
|
icon: UiIcons.calendar,
|
||||||
|
title: context.t.staff_shifts.my_shifts_tab.empty.title,
|
||||||
|
subtitle:
|
||||||
|
context.t.staff_shifts.my_shifts_tab.empty.subtitle,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: UiConstants.space32),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
/// A week-view calendar selector showing 7 days with navigation arrows.
|
||||||
|
///
|
||||||
|
/// Displays a month/year header with chevron arrows for week navigation and
|
||||||
|
/// a row of day cells. Days with assigned shifts show a dot indicator.
|
||||||
|
class WeekCalendarSelector extends StatelessWidget {
|
||||||
|
/// Creates a [WeekCalendarSelector].
|
||||||
|
const WeekCalendarSelector({
|
||||||
|
super.key,
|
||||||
|
required this.calendarDays,
|
||||||
|
required this.selectedDate,
|
||||||
|
required this.shifts,
|
||||||
|
required this.onDateSelected,
|
||||||
|
required this.onPreviousWeek,
|
||||||
|
required this.onNextWeek,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The 7 days to display in the calendar row.
|
||||||
|
final List<DateTime> calendarDays;
|
||||||
|
|
||||||
|
/// The currently selected date.
|
||||||
|
final DateTime selectedDate;
|
||||||
|
|
||||||
|
/// Assigned shifts used to show dot indicators on days with shifts.
|
||||||
|
final List<AssignedShift> shifts;
|
||||||
|
|
||||||
|
/// Called when a day cell is tapped.
|
||||||
|
final ValueChanged<DateTime> onDateSelected;
|
||||||
|
|
||||||
|
/// Called when the previous-week chevron is tapped.
|
||||||
|
final VoidCallback onPreviousWeek;
|
||||||
|
|
||||||
|
/// Called when the next-week chevron is tapped.
|
||||||
|
final VoidCallback onNextWeek;
|
||||||
|
|
||||||
|
bool _isSameDay(DateTime a, DateTime b) {
|
||||||
|
return a.year == b.year && a.month == b.month && a.day == b.day;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final DateTime weekStartDate = calendarDays.first;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
color: UiColors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: UiConstants.space4,
|
||||||
|
horizontal: UiConstants.space4,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
UiIcons.chevronLeft,
|
||||||
|
size: 20,
|
||||||
|
color: UiColors.textPrimary,
|
||||||
|
),
|
||||||
|
onPressed: onPreviousWeek,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
DateFormat('MMMM yyyy').format(weekStartDate),
|
||||||
|
style: UiTypography.title1m.textPrimary,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
UiIcons.chevronRight,
|
||||||
|
size: 20,
|
||||||
|
color: UiColors.textPrimary,
|
||||||
|
),
|
||||||
|
onPressed: onNextWeek,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Days Grid
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: calendarDays.map((DateTime date) {
|
||||||
|
final bool isSelected = _isSameDay(date, selectedDate);
|
||||||
|
final bool hasShifts = shifts.any(
|
||||||
|
(AssignedShift s) => _isSameDay(s.date, date),
|
||||||
|
);
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => onDateSelected(date),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
width: 44,
|
||||||
|
height: 60,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected ? UiColors.primary : UiColors.white,
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
UiConstants.radiusBase,
|
||||||
|
),
|
||||||
|
border: Border.all(
|
||||||
|
color:
|
||||||
|
isSelected ? UiColors.primary : UiColors.border,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
date.day.toString().padLeft(2, '0'),
|
||||||
|
style: isSelected
|
||||||
|
? UiTypography.body1b.white
|
||||||
|
: UiTypography.body1b.textPrimary,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
DateFormat('E').format(date),
|
||||||
|
style: (isSelected
|
||||||
|
? UiTypography.footnote2m.white
|
||||||
|
: UiTypography.footnote2m.textSecondary)
|
||||||
|
.copyWith(
|
||||||
|
color: isSelected
|
||||||
|
? UiColors.white.withValues(alpha: 0.8)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (hasShifts && !isSelected)
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(
|
||||||
|
top: UiConstants.space1,
|
||||||
|
),
|
||||||
|
width: 4,
|
||||||
|
height: 4,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: UiColors.primary,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
import 'package:core_localization/core_localization.dart';
|
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart' show ReadContext;
|
import 'package:flutter_bloc/flutter_bloc.dart' show ReadContext;
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:krow_core/core.dart';
|
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
import 'package:staff_shifts/src/domain/utils/shift_date_utils.dart';
|
import 'package:staff_shifts/src/domain/utils/shift_date_utils.dart';
|
||||||
import 'package:staff_shifts/src/presentation/blocs/shifts/shifts_bloc.dart';
|
import 'package:staff_shifts/src/presentation/blocs/shifts/shifts_bloc.dart';
|
||||||
import 'package:staff_shifts/src/presentation/widgets/shared/empty_state_view.dart';
|
import 'my_shifts/shift_section_list.dart';
|
||||||
import 'package:staff_shifts/src/presentation/widgets/shift_card/index.dart';
|
import 'my_shifts/week_calendar_selector.dart';
|
||||||
|
|
||||||
/// Tab displaying the worker's assigned, pending, and cancelled shifts.
|
/// Tab displaying the worker's assigned, pending, and cancelled shifts.
|
||||||
|
///
|
||||||
|
/// Manages the calendar selection state and delegates rendering to
|
||||||
|
/// [WeekCalendarSelector] and [ShiftSectionList].
|
||||||
class MyShiftsTab extends StatefulWidget {
|
class MyShiftsTab extends StatefulWidget {
|
||||||
/// Creates a [MyShiftsTab].
|
/// Creates a [MyShiftsTab].
|
||||||
const MyShiftsTab({
|
const MyShiftsTab({
|
||||||
@@ -133,270 +132,32 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
|
|||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
// Calendar Selector
|
WeekCalendarSelector(
|
||||||
Container(
|
calendarDays: calendarDays,
|
||||||
color: UiColors.white,
|
selectedDate: _selectedDate,
|
||||||
padding: const EdgeInsets.symmetric(
|
shifts: widget.myShifts,
|
||||||
vertical: UiConstants.space4,
|
onDateSelected: (DateTime date) =>
|
||||||
horizontal: UiConstants.space4,
|
setState(() => _selectedDate = date),
|
||||||
),
|
onPreviousWeek: () => setState(() {
|
||||||
child: Column(
|
|
||||||
children: <Widget>[
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: UiConstants.space3),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
UiIcons.chevronLeft,
|
|
||||||
size: 20,
|
|
||||||
color: UiColors.textPrimary,
|
|
||||||
),
|
|
||||||
onPressed: () => setState(() {
|
|
||||||
_weekOffset--;
|
_weekOffset--;
|
||||||
_selectedDate = _getCalendarDays().first;
|
_selectedDate = _getCalendarDays().first;
|
||||||
_loadShiftsForCurrentWeek();
|
_loadShiftsForCurrentWeek();
|
||||||
}),
|
}),
|
||||||
constraints: const BoxConstraints(),
|
onNextWeek: () => setState(() {
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
DateFormat('MMMM yyyy').format(weekStartDate),
|
|
||||||
style: UiTypography.title1m.textPrimary,
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
UiIcons.chevronRight,
|
|
||||||
size: 20,
|
|
||||||
color: UiColors.textPrimary,
|
|
||||||
),
|
|
||||||
onPressed: () => setState(() {
|
|
||||||
_weekOffset++;
|
_weekOffset++;
|
||||||
_selectedDate = _getCalendarDays().first;
|
_selectedDate = _getCalendarDays().first;
|
||||||
_loadShiftsForCurrentWeek();
|
_loadShiftsForCurrentWeek();
|
||||||
}),
|
}),
|
||||||
constraints: const BoxConstraints(),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Days Grid
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: calendarDays.map((DateTime date) {
|
|
||||||
final bool isSelected = _isSameDay(date, _selectedDate);
|
|
||||||
final bool hasShifts = widget.myShifts.any(
|
|
||||||
(AssignedShift s) => _isSameDay(s.date, date),
|
|
||||||
);
|
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () => setState(() => _selectedDate = date),
|
|
||||||
child: Column(
|
|
||||||
children: <Widget>[
|
|
||||||
Container(
|
|
||||||
width: 44,
|
|
||||||
height: 60,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isSelected
|
|
||||||
? UiColors.primary
|
|
||||||
: UiColors.white,
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
UiConstants.radiusBase,
|
|
||||||
),
|
|
||||||
border: Border.all(
|
|
||||||
color: isSelected
|
|
||||||
? UiColors.primary
|
|
||||||
: UiColors.border,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
date.day.toString().padLeft(2, '0'),
|
|
||||||
style: isSelected
|
|
||||||
? UiTypography.body1b.white
|
|
||||||
: UiTypography.body1b.textPrimary,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
DateFormat('E').format(date),
|
|
||||||
style: (isSelected
|
|
||||||
? UiTypography.footnote2m.white
|
|
||||||
: UiTypography
|
|
||||||
.footnote2m.textSecondary)
|
|
||||||
.copyWith(
|
|
||||||
color: isSelected
|
|
||||||
? UiColors.white
|
|
||||||
.withValues(alpha: 0.8)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (hasShifts && !isSelected)
|
|
||||||
Container(
|
|
||||||
margin: const EdgeInsets.only(
|
|
||||||
top: UiConstants.space1,
|
|
||||||
),
|
|
||||||
width: 4,
|
|
||||||
height: 4,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: UiColors.primary,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const Divider(height: 1, color: UiColors.border),
|
const Divider(height: 1, color: UiColors.border),
|
||||||
|
ShiftSectionList(
|
||||||
Expanded(
|
assignedShifts: visibleMyShifts,
|
||||||
child: SingleChildScrollView(
|
pendingAssignments: widget.pendingAssignments,
|
||||||
padding: const EdgeInsets.symmetric(
|
cancelledShifts: visibleCancelledShifts,
|
||||||
horizontal: UiConstants.space5,
|
submittedShiftIds: widget.submittedShiftIds,
|
||||||
),
|
submittingShiftId: widget.submittingShiftId,
|
||||||
child: Column(
|
|
||||||
children: <Widget>[
|
|
||||||
const SizedBox(height: UiConstants.space5),
|
|
||||||
if (widget.pendingAssignments.isNotEmpty) ...<Widget>[
|
|
||||||
_buildSectionHeader(
|
|
||||||
context
|
|
||||||
.t.staff_shifts.my_shifts_tab.sections.awaiting,
|
|
||||||
UiColors.textWarning,
|
|
||||||
),
|
|
||||||
...widget.pendingAssignments.map(
|
|
||||||
(PendingAssignment assignment) => Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
bottom: UiConstants.space4,
|
|
||||||
),
|
|
||||||
child: ShiftCard(
|
|
||||||
data: ShiftCardData.fromPending(assignment),
|
|
||||||
onTap: () => Modular.to
|
|
||||||
.toShiftDetailsById(assignment.shiftId),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space3),
|
|
||||||
],
|
|
||||||
|
|
||||||
if (visibleCancelledShifts.isNotEmpty) ...<Widget>[
|
|
||||||
_buildSectionHeader(
|
|
||||||
context
|
|
||||||
.t.staff_shifts.my_shifts_tab.sections.cancelled,
|
|
||||||
UiColors.textSecondary,
|
|
||||||
),
|
|
||||||
...visibleCancelledShifts.map(
|
|
||||||
(CancelledShift cs) => Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
bottom: UiConstants.space4,
|
|
||||||
),
|
|
||||||
child: ShiftCard(
|
|
||||||
data: ShiftCardData.fromCancelled(cs),
|
|
||||||
onTap: () =>
|
|
||||||
Modular.to.toShiftDetailsById(cs.shiftId),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space3),
|
|
||||||
],
|
|
||||||
|
|
||||||
// Confirmed Shifts
|
|
||||||
if (visibleMyShifts.isNotEmpty) ...<Widget>[
|
|
||||||
_buildSectionHeader(
|
|
||||||
context
|
|
||||||
.t.staff_shifts.my_shifts_tab.sections.confirmed,
|
|
||||||
UiColors.textSecondary,
|
|
||||||
),
|
|
||||||
...visibleMyShifts.map(
|
|
||||||
(AssignedShift shift) {
|
|
||||||
final bool isCompleted =
|
|
||||||
shift.status == AssignmentStatus.completed;
|
|
||||||
final bool isSubmitted =
|
|
||||||
widget.submittedShiftIds.contains(shift.shiftId);
|
|
||||||
final bool isSubmitting =
|
|
||||||
widget.submittingShiftId == shift.shiftId;
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
bottom: UiConstants.space3,
|
|
||||||
),
|
|
||||||
child: ShiftCard(
|
|
||||||
data: ShiftCardData.fromAssigned(shift),
|
|
||||||
onTap: () => Modular.to
|
|
||||||
.toShiftDetailsById(shift.shiftId),
|
|
||||||
showApprovalAction: isCompleted,
|
|
||||||
isSubmitted: isSubmitted,
|
|
||||||
isSubmitting: isSubmitting,
|
|
||||||
onSubmitForApproval: () {
|
|
||||||
ReadContext(context).read<ShiftsBloc>().add(
|
|
||||||
SubmitForApprovalEvent(
|
|
||||||
shiftId: shift.shiftId,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
UiSnackbar.show(
|
|
||||||
context,
|
|
||||||
message: context.t.staff_shifts
|
|
||||||
.my_shift_card.timesheet_submitted,
|
|
||||||
type: UiSnackbarType.success,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
|
|
||||||
if (visibleMyShifts.isEmpty &&
|
|
||||||
widget.pendingAssignments.isEmpty &&
|
|
||||||
widget.cancelledShifts.isEmpty)
|
|
||||||
EmptyStateView(
|
|
||||||
icon: UiIcons.calendar,
|
|
||||||
title:
|
|
||||||
context.t.staff_shifts.my_shifts_tab.empty.title,
|
|
||||||
subtitle: context
|
|
||||||
.t.staff_shifts.my_shifts_tab.empty.subtitle,
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: UiConstants.space32),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSectionHeader(String title, Color dotColor) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: UiConstants.space4),
|
|
||||||
child: Row(
|
|
||||||
children: <Widget>[
|
|
||||||
Container(
|
|
||||||
width: 8,
|
|
||||||
height: 8,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: dotColor,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space2),
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: (dotColor == UiColors.textSecondary
|
|
||||||
? UiTypography.body2b.textSecondary
|
|
||||||
: UiTypography.body2b.copyWith(color: dotColor)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user