diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card.dart deleted file mode 100644 index 63b83e93..00000000 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card.dart +++ /dev/null @@ -1,775 +0,0 @@ -import 'package:core_localization/core_localization.dart'; -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:krow_domain/krow_domain.dart'; - -/// Variant that controls the visual treatment of the [ShiftCard]. -/// -/// Each variant maps to a different colour scheme for the status badge and -/// optional footer action area. -enum ShiftCardVariant { - /// Confirmed / accepted assignment. - confirmed, - - /// Pending assignment awaiting acceptance. - pending, - - /// Cancelled assignment. - cancelled, - - /// Completed shift (history). - completed, - - /// Worker is currently checked in. - checkedIn, - - /// A swap has been requested. - swapRequested, -} - -/// Immutable data model that feeds the [ShiftCard]. -/// -/// Acts as an adapter between the various shift entity types -/// (`AssignedShift`, `CompletedShift`, `CancelledShift`, `PendingAssignment`) -/// and the unified card presentation. -class ShiftCardData { - /// Creates a [ShiftCardData]. - const ShiftCardData({ - required this.shiftId, - required this.title, - required this.location, - required this.date, - required this.variant, - this.subtitle, - this.startTime, - this.endTime, - this.hourlyRateCents, - this.orderType, - this.minutesWorked, - this.cancellationReason, - this.paymentStatus, - }); - - /// Constructs [ShiftCardData] from an [AssignedShift]. - factory ShiftCardData.fromAssigned(AssignedShift shift) { - return ShiftCardData( - shiftId: shift.shiftId, - title: shift.roleName, - subtitle: shift.location, - location: shift.location, - date: shift.date, - startTime: shift.startTime, - endTime: shift.endTime, - hourlyRateCents: shift.hourlyRateCents, - orderType: shift.orderType, - variant: _variantFromAssignmentStatus(shift.status), - ); - } - - /// Constructs [ShiftCardData] from a [CompletedShift]. - factory ShiftCardData.fromCompleted(CompletedShift shift) { - return ShiftCardData( - shiftId: shift.shiftId, - title: shift.title, - location: shift.location, - date: shift.date, - minutesWorked: shift.minutesWorked, - paymentStatus: shift.paymentStatus, - variant: ShiftCardVariant.completed, - ); - } - - /// Constructs [ShiftCardData] from a [CancelledShift]. - factory ShiftCardData.fromCancelled(CancelledShift shift) { - return ShiftCardData( - shiftId: shift.shiftId, - title: shift.title, - location: shift.location, - date: shift.date, - cancellationReason: shift.cancellationReason, - variant: ShiftCardVariant.cancelled, - ); - } - - /// Constructs [ShiftCardData] from a [PendingAssignment]. - factory ShiftCardData.fromPending(PendingAssignment assignment) { - return ShiftCardData( - shiftId: assignment.shiftId, - title: assignment.roleName, - subtitle: assignment.title.isNotEmpty ? assignment.title : null, - location: assignment.location, - date: assignment.startTime, - startTime: assignment.startTime, - endTime: assignment.endTime, - variant: ShiftCardVariant.pending, - ); - } - - /// The shift row id. - final String shiftId; - - /// Primary display title (role name or shift title). - final String title; - - /// Optional secondary text (e.g. location under the role name). - final String? subtitle; - - /// Human-readable location label. - final String location; - - /// The date of the shift. - final DateTime date; - - /// Scheduled start time (null for completed/cancelled). - final DateTime? startTime; - - /// Scheduled end time (null for completed/cancelled). - final DateTime? endTime; - - /// Hourly pay rate in cents (null when not applicable). - final int? hourlyRateCents; - - /// Order type (null for completed/cancelled). - final OrderType? orderType; - - /// Minutes worked (only for completed shifts). - final int? minutesWorked; - - /// Cancellation reason (only for cancelled shifts). - final String? cancellationReason; - - /// Payment processing status (only for completed shifts). - final PaymentStatus? paymentStatus; - - /// Visual variant for the card. - final ShiftCardVariant variant; - - static ShiftCardVariant _variantFromAssignmentStatus( - AssignmentStatus status, - ) { - switch (status) { - case AssignmentStatus.accepted: - return ShiftCardVariant.confirmed; - case AssignmentStatus.checkedIn: - return ShiftCardVariant.checkedIn; - case AssignmentStatus.swapRequested: - return ShiftCardVariant.swapRequested; - case AssignmentStatus.completed: - return ShiftCardVariant.completed; - case AssignmentStatus.cancelled: - return ShiftCardVariant.cancelled; - case AssignmentStatus.assigned: - return ShiftCardVariant.pending; - case AssignmentStatus.checkedOut: - case AssignmentStatus.noShow: - case AssignmentStatus.unknown: - return ShiftCardVariant.confirmed; - } - } -} - -/// Unified card widget for displaying shift information across all shift types. -/// -/// Replaces `MyShiftCard`, `ShiftAssignmentCard`, and the inline -/// `_CompletedShiftCard` / `_buildCancelledCard` from the tabs. Accepts a -/// [ShiftCardData] data model that adapts the various domain entities into a -/// common display shape. -class ShiftCard extends StatelessWidget { - /// Creates a [ShiftCard]. - const ShiftCard({ - super.key, - required this.data, - this.onTap, - this.onSubmitForApproval, - this.showApprovalAction = false, - this.isSubmitted = false, - this.onAccept, - this.onDecline, - this.isAccepting = false, - }); - - /// The shift data to display. - final ShiftCardData data; - - /// Callback when the card is tapped (typically navigates to shift details). - final VoidCallback? onTap; - - /// Callback when the "Submit for Approval" button is pressed. - final VoidCallback? onSubmitForApproval; - - /// Whether to show the submit-for-approval footer. - final bool showApprovalAction; - - /// Whether the timesheet has already been submitted. - final bool isSubmitted; - - /// Callback when the accept action is pressed (pending assignments only). - final VoidCallback? onAccept; - - /// Callback when the decline action is pressed (pending assignments only). - final VoidCallback? onDecline; - - /// Whether the accept action is in progress. - final bool isAccepting; - - /// Whether the accept/decline footer should be shown. - bool get _showPendingActions => onAccept != null || onDecline != null; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Container( - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusLg, - border: Border.all(color: UiColors.border), - boxShadow: _showPendingActions - ? [ - BoxShadow( - color: UiColors.black.withValues(alpha: 0.05), - blurRadius: 2, - offset: const Offset(0, 1), - ), - ] - : null, - ), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(UiConstants.space4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _StatusBadge( - variant: data.variant, - orderType: data.orderType, - ), - const SizedBox(height: UiConstants.space2), - _CardBody(data: data), - if (showApprovalAction) ...[ - const SizedBox(height: UiConstants.space4), - const Divider(height: 1, color: UiColors.border), - const SizedBox(height: UiConstants.space2), - _ApprovalFooter( - isSubmitted: isSubmitted, - onSubmit: onSubmitForApproval, - ), - ], - ], - ), - ), - if (_showPendingActions) - _PendingActionsFooter( - onAccept: onAccept, - onDecline: onDecline, - isAccepting: isAccepting, - ), - ], - ), - ), - ); - } -} - -/// Displays the coloured status dot/icon and label, plus an optional order-type -/// chip. -class _StatusBadge extends StatelessWidget { - const _StatusBadge({required this.variant, this.orderType}); - - final ShiftCardVariant variant; - final OrderType? orderType; - - @override - Widget build(BuildContext context) { - final _StatusStyle style = _resolveStyle(context); - - return Row( - children: [ - if (style.icon != null) - Padding( - padding: const EdgeInsets.only(right: UiConstants.space2), - child: Icon( - style.icon, - size: UiConstants.iconXs, - color: style.foreground, - ), - ) - else - Container( - width: 8, - height: 8, - margin: const EdgeInsets.only(right: UiConstants.space2), - decoration: BoxDecoration( - color: style.dot, - shape: BoxShape.circle, - ), - ), - Text( - style.label, - style: UiTypography.footnote2b.copyWith( - color: style.foreground, - letterSpacing: 0.5, - ), - ), - if (orderType != null) ...[ - const SizedBox(width: UiConstants.space2), - _OrderTypeChip(orderType: orderType!), - ], - ], - ); - } - - _StatusStyle _resolveStyle(BuildContext context) { - switch (variant) { - case ShiftCardVariant.confirmed: - return _StatusStyle( - label: context.t.staff_shifts.status.confirmed, - foreground: UiColors.textLink, - dot: UiColors.primary, - ); - case ShiftCardVariant.pending: - return _StatusStyle( - label: context.t.staff_shifts.status.act_now, - foreground: UiColors.destructive, - dot: UiColors.destructive, - ); - case ShiftCardVariant.cancelled: - return _StatusStyle( - label: context.t.staff_shifts.my_shifts_tab.card.cancelled, - foreground: UiColors.destructive, - dot: UiColors.destructive, - ); - case ShiftCardVariant.completed: - return _StatusStyle( - label: context.t.staff_shifts.status.completed, - foreground: UiColors.textSuccess, - dot: UiColors.iconSuccess, - ); - case ShiftCardVariant.checkedIn: - return _StatusStyle( - label: context.t.staff_shifts.my_shift_card.checked_in, - foreground: UiColors.textSuccess, - dot: UiColors.iconSuccess, - ); - case ShiftCardVariant.swapRequested: - return _StatusStyle( - label: context.t.staff_shifts.status.swap_requested, - foreground: UiColors.textWarning, - dot: UiColors.textWarning, - icon: UiIcons.swap, - ); - } - } -} - -/// Internal helper grouping status badge presentation values. -class _StatusStyle { - const _StatusStyle({ - required this.label, - required this.foreground, - required this.dot, - this.icon, - }); - - final String label; - final Color foreground; - final Color dot; - final IconData? icon; -} - -/// Small chip showing the order type (One Day / Multi-Day / Long Term). -class _OrderTypeChip extends StatelessWidget { - const _OrderTypeChip({required this.orderType}); - - final OrderType orderType; - - @override - Widget build(BuildContext context) { - final String label = _label(context); - - return Container( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space2, - vertical: 2, - ), - decoration: BoxDecoration( - color: UiColors.background, - borderRadius: UiConstants.radiusSm, - border: Border.all(color: UiColors.border), - ), - child: Text( - label, - style: UiTypography.footnote2m.copyWith(color: UiColors.textSecondary), - ), - ); - } - - String _label(BuildContext context) { - switch (orderType) { - case OrderType.permanent: - return context.t.staff_shifts.filter.long_term; - case OrderType.recurring: - return context.t.staff_shifts.filter.multi_day; - case OrderType.oneTime: - case OrderType.rapid: - case OrderType.unknown: - return context.t.staff_shifts.filter.one_day; - } - } -} - -/// The main body: icon, title/subtitle, metadata rows, and optional pay info. -class _CardBody extends StatelessWidget { - const _CardBody({required this.data}); - - final ShiftCardData data; - - @override - Widget build(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _ShiftIcon(variant: data.variant), - const SizedBox(width: UiConstants.space3), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _TitleRow(data: data), - if (data.subtitle != null) ...[ - Text( - data.subtitle!, - style: UiTypography.body3r.textSecondary, - overflow: TextOverflow.ellipsis, - ), - ], - const SizedBox(height: UiConstants.space2), - _MetadataRows(data: data), - if (data.cancellationReason != null && - data.cancellationReason!.isNotEmpty) ...[ - const SizedBox(height: UiConstants.space1), - Text( - data.cancellationReason!, - style: UiTypography.footnote2r.textSecondary, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], - ], - ), - ), - ], - ); - } -} - -/// The 44x44 icon box with a gradient background. -class _ShiftIcon extends StatelessWidget { - const _ShiftIcon({required this.variant}); - - final ShiftCardVariant variant; - - @override - Widget build(BuildContext context) { - final bool isCancelled = variant == ShiftCardVariant.cancelled; - - return Container( - width: 44, - height: 44, - decoration: BoxDecoration( - gradient: isCancelled - ? null - : LinearGradient( - colors: [ - UiColors.primary.withValues(alpha: 0.09), - UiColors.primary.withValues(alpha: 0.03), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - color: isCancelled - ? UiColors.primary.withValues(alpha: 0.05) - : null, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - border: isCancelled - ? null - : Border.all( - color: UiColors.primary.withValues(alpha: 0.09), - ), - ), - child: const Center( - child: Icon( - UiIcons.briefcase, - color: UiColors.primary, - size: UiConstants.iconMd, - ), - ), - ); - } -} - -/// Title row with optional pay summary on the right. -class _TitleRow extends StatelessWidget { - const _TitleRow({required this.data}); - - final ShiftCardData data; - - @override - Widget build(BuildContext context) { - final bool hasPay = data.hourlyRateCents != null && - data.startTime != null && - data.endTime != null; - - if (!hasPay) { - return Text( - data.title, - style: UiTypography.body2m.textPrimary, - overflow: TextOverflow.ellipsis, - ); - } - - final double hourlyRate = data.hourlyRateCents! / 100; - final int durationMinutes = - data.endTime!.difference(data.startTime!).inMinutes; - double durationHours = durationMinutes / 60; - if (durationHours < 0) durationHours += 24; - durationHours = durationHours.roundToDouble(); - final double estimatedTotal = hourlyRate * durationHours; - - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - data.title, - style: UiTypography.body2m.textPrimary, - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: UiConstants.space2), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - '\$${estimatedTotal.toStringAsFixed(0)}', - style: UiTypography.title1m.textPrimary, - ), - Text( - '\$${hourlyRate.toInt()}/hr \u00b7 ${durationHours.toInt()}h', - style: UiTypography.footnote2r.textSecondary, - ), - ], - ), - ], - ); - } -} - -/// Date, time, location, and worked-hours rows. -class _MetadataRows extends StatelessWidget { - const _MetadataRows({required this.data}); - - final ShiftCardData data; - - @override - Widget build(BuildContext context) { - return Column( - children: [ - // Date and time row - Row( - children: [ - const Icon( - UiIcons.calendar, - size: UiConstants.iconXs, - color: UiColors.iconSecondary, - ), - const SizedBox(width: UiConstants.space1), - Text( - _formatDate(context, data.date), - style: UiTypography.footnote1r.textSecondary, - ), - if (data.startTime != null && data.endTime != null) ...[ - const SizedBox(width: UiConstants.space3), - const Icon( - UiIcons.clock, - size: UiConstants.iconXs, - color: UiColors.iconSecondary, - ), - const SizedBox(width: UiConstants.space1), - Text( - '${_formatTime(data.startTime!)} - ${_formatTime(data.endTime!)}', - style: UiTypography.footnote1r.textSecondary, - ), - ], - if (data.minutesWorked != null) ...[ - const SizedBox(width: UiConstants.space3), - const Icon( - UiIcons.clock, - size: UiConstants.iconXs, - color: UiColors.iconSecondary, - ), - const SizedBox(width: UiConstants.space1), - Text( - _formatWorkedDuration(data.minutesWorked!), - style: UiTypography.footnote1r.textSecondary, - ), - ], - ], - ), - const SizedBox(height: UiConstants.space1), - // Location row - Row( - children: [ - const Icon( - UiIcons.mapPin, - size: UiConstants.iconXs, - color: UiColors.iconSecondary, - ), - const SizedBox(width: UiConstants.space1), - Expanded( - child: Text( - data.location, - style: UiTypography.footnote1r.textSecondary, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ], - ); - } - - String _formatDate(BuildContext context, DateTime date) { - final DateTime now = DateTime.now(); - final DateTime today = DateTime(now.year, now.month, now.day); - final DateTime tomorrow = today.add(const Duration(days: 1)); - final DateTime d = DateTime(date.year, date.month, date.day); - if (d == today) return context.t.staff_shifts.my_shifts_tab.date.today; - if (d == tomorrow) { - return context.t.staff_shifts.my_shifts_tab.date.tomorrow; - } - return DateFormat('EEE, MMM d').format(date); - } - - String _formatTime(DateTime dt) => DateFormat('h:mm a').format(dt); - - String _formatWorkedDuration(int totalMinutes) { - final int hours = totalMinutes ~/ 60; - final int mins = totalMinutes % 60; - return mins > 0 ? '${hours}h ${mins}m' : '${hours}h'; - } -} - -/// Footer showing the submit-for-approval action for completed shifts. -class _ApprovalFooter extends StatelessWidget { - const _ApprovalFooter({ - required this.isSubmitted, - this.onSubmit, - }); - - final bool isSubmitted; - final VoidCallback? onSubmit; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - isSubmitted - ? context.t.staff_shifts.my_shift_card.submitted - : context.t.staff_shifts.my_shift_card.ready_to_submit, - style: UiTypography.footnote2b.copyWith( - color: isSubmitted ? UiColors.textSuccess : UiColors.textSecondary, - ), - ), - if (!isSubmitted) - UiButton.secondary( - text: context.t.staff_shifts.my_shift_card.submit_for_approval, - size: UiButtonSize.small, - onPressed: onSubmit, - ) - else - const Icon( - UiIcons.success, - color: UiColors.iconSuccess, - size: 20, - ), - ], - ); - } -} - -/// Coloured footer with Decline / Accept buttons for pending assignments. -class _PendingActionsFooter extends StatelessWidget { - const _PendingActionsFooter({ - this.onAccept, - this.onDecline, - this.isAccepting = false, - }); - - final VoidCallback? onAccept; - final VoidCallback? onDecline; - final bool isAccepting; - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(UiConstants.space2), - decoration: const BoxDecoration( - color: UiColors.secondary, - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(UiConstants.radiusBase), - bottomRight: Radius.circular(UiConstants.radiusBase), - ), - ), - child: Row( - children: [ - Expanded( - child: TextButton( - onPressed: onDecline, - style: TextButton.styleFrom( - foregroundColor: UiColors.destructive, - ), - child: Text( - context.t.staff_shifts.action.decline, - style: UiTypography.body2m.textError, - ), - ), - ), - const SizedBox(width: UiConstants.space2), - Expanded( - child: ElevatedButton( - onPressed: isAccepting ? null : onAccept, - style: ElevatedButton.styleFrom( - backgroundColor: UiColors.primary, - foregroundColor: UiColors.white, - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - UiConstants.radiusMdValue, - ), - ), - ), - child: isAccepting - ? const SizedBox( - height: 16, - width: 16, - child: CircularProgressIndicator( - strokeWidth: 2, - color: UiColors.white, - ), - ) - : Text( - context.t.staff_shifts.action.confirm, - style: UiTypography.body2m.white, - ), - ), - ), - ], - ), - ); - } -} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/index.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/index.dart new file mode 100644 index 00000000..3c934a1a --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/index.dart @@ -0,0 +1,8 @@ +export 'shift_card.dart'; +export 'shift_card_approval_footer.dart'; +export 'shift_card_body.dart'; +export 'shift_card_data.dart'; +export 'shift_card_metadata_rows.dart'; +export 'shift_card_pending_footer.dart'; +export 'shift_card_status_badge.dart'; +export 'shift_card_title_row.dart'; diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card.dart new file mode 100644 index 00000000..aa5a6b4d --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card.dart @@ -0,0 +1,111 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:staff_shifts/src/presentation/widgets/shift_card/shift_card_approval_footer.dart'; +import 'package:staff_shifts/src/presentation/widgets/shift_card/shift_card_body.dart'; +import 'package:staff_shifts/src/presentation/widgets/shift_card/shift_card_data.dart'; +import 'package:staff_shifts/src/presentation/widgets/shift_card/shift_card_pending_footer.dart'; +import 'package:staff_shifts/src/presentation/widgets/shift_card/shift_card_status_badge.dart'; + +/// Unified card widget for displaying shift information across all shift types. +/// +/// Replaces `MyShiftCard`, `ShiftAssignmentCard`, and the inline +/// `_CompletedShiftCard` / `_buildCancelledCard` from the tabs. Accepts a +/// [ShiftCardData] data model that adapts the various domain entities into a +/// common display shape. +class ShiftCard extends StatelessWidget { + /// Creates a [ShiftCard]. + const ShiftCard({ + super.key, + required this.data, + this.onTap, + this.onSubmitForApproval, + this.showApprovalAction = false, + this.isSubmitted = false, + this.onAccept, + this.onDecline, + this.isAccepting = false, + }); + + /// The shift data to display. + final ShiftCardData data; + + /// Callback when the card is tapped (typically navigates to shift details). + final VoidCallback? onTap; + + /// Callback when the "Submit for Approval" button is pressed. + final VoidCallback? onSubmitForApproval; + + /// Whether to show the submit-for-approval footer. + final bool showApprovalAction; + + /// Whether the timesheet has already been submitted. + final bool isSubmitted; + + /// Callback when the accept action is pressed (pending assignments only). + final VoidCallback? onAccept; + + /// Callback when the decline action is pressed (pending assignments only). + final VoidCallback? onDecline; + + /// Whether the accept action is in progress. + final bool isAccepting; + + /// Whether the accept/decline footer should be shown. + bool get _showPendingActions => onAccept != null || onDecline != null; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusLg, + border: Border.all(color: UiColors.border, width: 0.5), + boxShadow: _showPendingActions + ? [ + BoxShadow( + color: UiColors.black.withValues(alpha: 0.05), + blurRadius: 2, + offset: const Offset(0, 1), + ), + ] + : null, + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(UiConstants.space4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ShiftCardStatusBadge( + variant: data.variant, + orderType: data.orderType, + ), + const SizedBox(height: UiConstants.space2), + ShiftCardBody(data: data), + if (showApprovalAction) ...[ + const SizedBox(height: UiConstants.space4), + const Divider(height: 1, color: UiColors.border), + const SizedBox(height: UiConstants.space2), + ShiftCardApprovalFooter( + isSubmitted: isSubmitted, + onSubmit: onSubmitForApproval, + ), + ], + ], + ), + ), + if (_showPendingActions) + ShiftCardPendingActionsFooter( + onAccept: onAccept, + onDecline: onDecline, + isAccepting: isAccepting, + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_approval_footer.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_approval_footer.dart new file mode 100644 index 00000000..60794010 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_approval_footer.dart @@ -0,0 +1,44 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Footer showing the submit-for-approval action for completed shifts. +class ShiftCardApprovalFooter extends StatelessWidget { + /// Creates a [ShiftCardApprovalFooter]. + const ShiftCardApprovalFooter({ + super.key, + required this.isSubmitted, + this.onSubmit, + }); + + /// Whether the timesheet has already been submitted. + final bool isSubmitted; + + /// Callback when the submit button is pressed. + final VoidCallback? onSubmit; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + isSubmitted + ? context.t.staff_shifts.my_shift_card.submitted + : context.t.staff_shifts.my_shift_card.ready_to_submit, + style: UiTypography.footnote2b.copyWith( + color: isSubmitted ? UiColors.textSuccess : UiColors.textSecondary, + ), + ), + if (!isSubmitted) + UiButton.secondary( + text: context.t.staff_shifts.my_shift_card.submit_for_approval, + size: UiButtonSize.small, + onPressed: onSubmit, + ) + else + const Icon(UiIcons.success, color: UiColors.iconSuccess, size: 20), + ], + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_body.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_body.dart new file mode 100644 index 00000000..0816d430 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_body.dart @@ -0,0 +1,88 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:staff_shifts/src/presentation/widgets/shift_card/shift_card_data.dart'; +import 'package:staff_shifts/src/presentation/widgets/shift_card/shift_card_metadata_rows.dart'; +import 'package:staff_shifts/src/presentation/widgets/shift_card/shift_card_title_row.dart'; + +/// The main body: icon, title/subtitle, metadata rows, and optional pay info. +class ShiftCardBody extends StatelessWidget { + /// Creates a [ShiftCardBody]. + const ShiftCardBody({super.key, required this.data}); + + /// The shift data to display. + final ShiftCardData data; + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ShiftCardIcon(variant: data.variant), + const SizedBox(width: UiConstants.space3), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ShiftCardTitleRow(data: data), + const SizedBox(height: UiConstants.space2), + ShiftCardMetadataRows(data: data), + if (data.cancellationReason != null && + data.cancellationReason!.isNotEmpty) ...[ + const SizedBox(height: UiConstants.space1), + Text( + data.cancellationReason!, + style: UiTypography.footnote2r.textSecondary, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ), + ], + ); + } +} + +/// The 44x44 icon box with a gradient background. +class ShiftCardIcon extends StatelessWidget { + /// Creates a [ShiftCardIcon]. + const ShiftCardIcon({super.key, required this.variant}); + + /// The variant controlling the icon appearance. + final ShiftCardVariant variant; + + @override + Widget build(BuildContext context) { + final bool isCancelled = variant == ShiftCardVariant.cancelled; + + return Container( + width: 44, + height: 44, + decoration: BoxDecoration( + gradient: isCancelled + ? null + : LinearGradient( + colors: [ + UiColors.primary.withValues(alpha: 0.09), + UiColors.primary.withValues(alpha: 0.03), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + color: isCancelled ? UiColors.primary.withValues(alpha: 0.05) : null, + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + border: isCancelled + ? null + : Border.all(color: UiColors.primary.withValues(alpha: 0.09)), + ), + child: const Center( + child: Icon( + UiIcons.briefcase, + color: UiColors.primary, + size: UiConstants.iconMd, + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_data.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_data.dart new file mode 100644 index 00000000..626ff583 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_data.dart @@ -0,0 +1,180 @@ +import 'package:krow_domain/krow_domain.dart'; + +/// Variant that controls the visual treatment of the [ShiftCard]. +/// +/// Each variant maps to a different colour scheme for the status badge and +/// optional footer action area. +enum ShiftCardVariant { + /// Confirmed / accepted assignment. + confirmed, + + /// Pending assignment awaiting acceptance. + pending, + + /// Cancelled assignment. + cancelled, + + /// Completed shift (history). + completed, + + /// Worker is currently checked in. + checkedIn, + + /// A swap has been requested. + swapRequested, +} + +/// Immutable data model that feeds the [ShiftCard]. +/// +/// Acts as an adapter between the various shift entity types +/// (`AssignedShift`, `CompletedShift`, `CancelledShift`, `PendingAssignment`) +/// and the unified card presentation. +class ShiftCardData { + /// Creates a [ShiftCardData]. + const ShiftCardData({ + required this.shiftId, + required this.title, + required this.location, + required this.date, + required this.variant, + this.subtitle, + this.startTime, + this.endTime, + this.hourlyRateCents, + this.hourlyRate, + this.totalRate, + this.orderType, + this.minutesWorked, + this.cancellationReason, + this.paymentStatus, + }); + + /// Constructs [ShiftCardData] from an [AssignedShift]. + factory ShiftCardData.fromAssigned(AssignedShift shift) { + return ShiftCardData( + shiftId: shift.shiftId, + title: shift.roleName, + subtitle: shift.location, + location: shift.location, + date: shift.date, + startTime: shift.startTime, + endTime: shift.endTime, + hourlyRateCents: shift.hourlyRateCents, + orderType: shift.orderType, + variant: _variantFromAssignmentStatus(shift.status), + ); + } + + /// Constructs [ShiftCardData] from a [CompletedShift]. + factory ShiftCardData.fromCompleted(CompletedShift shift) { + return ShiftCardData( + shiftId: shift.shiftId, + title: shift.clientName.isNotEmpty ? shift.clientName : shift.title, + subtitle: shift.title.isNotEmpty ? shift.title : null, + location: shift.location, + date: shift.date, + startTime: shift.startTime, + endTime: shift.endTime, + hourlyRateCents: shift.hourlyRateCents, + hourlyRate: shift.hourlyRate, + totalRate: shift.totalRate, + minutesWorked: shift.minutesWorked, + paymentStatus: shift.paymentStatus, + variant: ShiftCardVariant.completed, + ); + } + + /// Constructs [ShiftCardData] from a [CancelledShift]. + factory ShiftCardData.fromCancelled(CancelledShift shift) { + return ShiftCardData( + shiftId: shift.shiftId, + title: shift.title, + location: shift.location, + date: shift.date, + cancellationReason: shift.cancellationReason, + variant: ShiftCardVariant.cancelled, + ); + } + + /// Constructs [ShiftCardData] from a [PendingAssignment]. + factory ShiftCardData.fromPending(PendingAssignment assignment) { + return ShiftCardData( + shiftId: assignment.shiftId, + title: assignment.roleName, + subtitle: assignment.title.isNotEmpty ? assignment.title : null, + location: assignment.location, + date: assignment.startTime, + startTime: assignment.startTime, + endTime: assignment.endTime, + variant: ShiftCardVariant.pending, + ); + } + + /// The shift row id. + final String shiftId; + + /// Primary display title (role name or shift title). + final String title; + + /// Optional secondary text (e.g. location under the role name). + final String? subtitle; + + /// Human-readable location label. + final String location; + + /// The date of the shift. + final DateTime date; + + /// Scheduled start time (null for completed/cancelled). + final DateTime? startTime; + + /// Scheduled end time (null for completed/cancelled). + final DateTime? endTime; + + /// Hourly pay rate in cents (null when not applicable). + final int? hourlyRateCents; + + /// Hourly pay rate in dollars (null when not applicable). + final double? hourlyRate; + + /// Total pay in dollars (null when not applicable). + final double? totalRate; + + /// Order type (null for completed/cancelled). + final OrderType? orderType; + + /// Minutes worked (only for completed shifts). + final int? minutesWorked; + + /// Cancellation reason (only for cancelled shifts). + final String? cancellationReason; + + /// Payment processing status (only for completed shifts). + final PaymentStatus? paymentStatus; + + /// Visual variant for the card. + final ShiftCardVariant variant; + + static ShiftCardVariant _variantFromAssignmentStatus( + AssignmentStatus status, + ) { + switch (status) { + case AssignmentStatus.accepted: + return ShiftCardVariant.confirmed; + case AssignmentStatus.checkedIn: + return ShiftCardVariant.checkedIn; + case AssignmentStatus.swapRequested: + return ShiftCardVariant.swapRequested; + case AssignmentStatus.completed: + return ShiftCardVariant.completed; + case AssignmentStatus.cancelled: + return ShiftCardVariant.cancelled; + case AssignmentStatus.assigned: + return ShiftCardVariant.pending; + case AssignmentStatus.checkedOut: + case AssignmentStatus.noShow: + case AssignmentStatus.unknown: + return ShiftCardVariant.confirmed; + } + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_metadata_rows.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_metadata_rows.dart new file mode 100644 index 00000000..df0ce572 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_metadata_rows.dart @@ -0,0 +1,102 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:staff_shifts/src/presentation/widgets/shift_card/shift_card_data.dart'; + +/// Date, time, location, and worked-hours rows. +class ShiftCardMetadataRows extends StatelessWidget { + /// Creates a [ShiftCardMetadataRows]. + const ShiftCardMetadataRows({super.key, required this.data}); + + /// The shift data to display. + final ShiftCardData data; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // Date and time row + Row( + children: [ + const Icon( + UiIcons.calendar, + size: UiConstants.iconXs, + color: UiColors.iconSecondary, + ), + const SizedBox(width: UiConstants.space1), + Text( + _formatDate(context, data.date), + style: UiTypography.footnote1r.textSecondary, + ), + if (data.startTime != null && data.endTime != null) ...[ + const SizedBox(width: UiConstants.space3), + const Icon( + UiIcons.clock, + size: UiConstants.iconXs, + color: UiColors.iconSecondary, + ), + const SizedBox(width: UiConstants.space1), + Text( + '${_formatTime(data.startTime!)} - ${_formatTime(data.endTime!)}', + style: UiTypography.footnote1r.textSecondary, + ), + ], + if (data.minutesWorked != null) ...[ + const SizedBox(width: UiConstants.space3), + const Icon( + UiIcons.clock, + size: UiConstants.iconXs, + color: UiColors.iconSecondary, + ), + const SizedBox(width: UiConstants.space1), + Text( + _formatWorkedDuration(data.minutesWorked!), + style: UiTypography.footnote1r.textSecondary, + ), + ], + ], + ), + const SizedBox(height: UiConstants.space1), + // Location row + Row( + children: [ + const Icon( + UiIcons.mapPin, + size: UiConstants.iconXs, + color: UiColors.iconSecondary, + ), + const SizedBox(width: UiConstants.space1), + Expanded( + child: Text( + data.location, + style: UiTypography.footnote1r.textSecondary, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ); + } + + String _formatDate(BuildContext context, DateTime date) { + final DateTime now = DateTime.now(); + final DateTime today = DateTime(now.year, now.month, now.day); + final DateTime tomorrow = today.add(const Duration(days: 1)); + final DateTime d = DateTime(date.year, date.month, date.day); + if (d == today) return context.t.staff_shifts.my_shifts_tab.date.today; + if (d == tomorrow) { + return context.t.staff_shifts.my_shifts_tab.date.tomorrow; + } + return DateFormat('EEE, MMM d').format(date); + } + + String _formatTime(DateTime dt) => DateFormat('h:mm a').format(dt); + + String _formatWorkedDuration(int totalMinutes) { + final int hours = totalMinutes ~/ 60; + final int mins = totalMinutes % 60; + return mins > 0 ? '${hours}h ${mins}m' : '${hours}h'; + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_pending_footer.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_pending_footer.dart new file mode 100644 index 00000000..dd9519c2 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_pending_footer.dart @@ -0,0 +1,82 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Coloured footer with Decline / Accept buttons for pending assignments. +class ShiftCardPendingActionsFooter extends StatelessWidget { + /// Creates a [ShiftCardPendingActionsFooter]. + const ShiftCardPendingActionsFooter({ + super.key, + this.onAccept, + this.onDecline, + this.isAccepting = false, + }); + + /// Callback when the accept action is pressed. + final VoidCallback? onAccept; + + /// Callback when the decline action is pressed. + final VoidCallback? onDecline; + + /// Whether the accept action is in progress. + final bool isAccepting; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space2), + decoration: const BoxDecoration( + color: UiColors.secondary, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(UiConstants.radiusBase), + bottomRight: Radius.circular(UiConstants.radiusBase), + ), + ), + child: Row( + children: [ + Expanded( + child: TextButton( + onPressed: onDecline, + style: TextButton.styleFrom( + foregroundColor: UiColors.destructive, + ), + child: Text( + context.t.staff_shifts.action.decline, + style: UiTypography.body2m.textError, + ), + ), + ), + const SizedBox(width: UiConstants.space2), + Expanded( + child: ElevatedButton( + onPressed: isAccepting ? null : onAccept, + style: ElevatedButton.styleFrom( + backgroundColor: UiColors.primary, + foregroundColor: UiColors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + UiConstants.radiusMdValue, + ), + ), + ), + child: isAccepting + ? const SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: UiColors.white, + ), + ) + : Text( + context.t.staff_shifts.action.confirm, + style: UiTypography.body2m.white, + ), + ), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_status_badge.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_status_badge.dart new file mode 100644 index 00000000..85465ea3 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_status_badge.dart @@ -0,0 +1,163 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:krow_domain/krow_domain.dart'; +import 'package:staff_shifts/src/presentation/widgets/shift_card/shift_card_data.dart'; + +/// Displays the coloured status dot/icon and label, plus an optional order-type +/// chip. +class ShiftCardStatusBadge extends StatelessWidget { + /// Creates a [ShiftCardStatusBadge]. + const ShiftCardStatusBadge({super.key, required this.variant, this.orderType}); + + /// The visual variant for colour resolution. + final ShiftCardVariant variant; + + /// Optional order type shown as a trailing chip. + final OrderType? orderType; + + @override + Widget build(BuildContext context) { + final ShiftCardStatusStyle style = _resolveStyle(context); + + return Row( + children: [ + if (style.icon != null) + Padding( + padding: const EdgeInsets.only(right: UiConstants.space2), + child: Icon( + style.icon, + size: UiConstants.iconXs, + color: style.foreground, + ), + ) + else + Container( + width: 8, + height: 8, + margin: const EdgeInsets.only(right: UiConstants.space2), + decoration: BoxDecoration(color: style.dot, shape: BoxShape.circle), + ), + Text( + style.label, + style: UiTypography.footnote2b.copyWith( + color: style.foreground, + letterSpacing: 0.5, + ), + ), + if (orderType != null) ...[ + const SizedBox(width: UiConstants.space2), + ShiftCardOrderTypeChip(orderType: orderType!), + ], + ], + ); + } + + ShiftCardStatusStyle _resolveStyle(BuildContext context) { + switch (variant) { + case ShiftCardVariant.confirmed: + return ShiftCardStatusStyle( + label: context.t.staff_shifts.status.confirmed, + foreground: UiColors.textLink, + dot: UiColors.primary, + ); + case ShiftCardVariant.pending: + return ShiftCardStatusStyle( + label: context.t.staff_shifts.status.act_now, + foreground: UiColors.destructive, + dot: UiColors.destructive, + ); + case ShiftCardVariant.cancelled: + return ShiftCardStatusStyle( + label: context.t.staff_shifts.my_shifts_tab.card.cancelled, + foreground: UiColors.destructive, + dot: UiColors.destructive, + ); + case ShiftCardVariant.completed: + return ShiftCardStatusStyle( + label: context.t.staff_shifts.status.completed, + foreground: UiColors.textSuccess, + dot: UiColors.iconSuccess, + ); + case ShiftCardVariant.checkedIn: + return ShiftCardStatusStyle( + label: context.t.staff_shifts.my_shift_card.checked_in, + foreground: UiColors.textSuccess, + dot: UiColors.iconSuccess, + ); + case ShiftCardVariant.swapRequested: + return ShiftCardStatusStyle( + label: context.t.staff_shifts.status.swap_requested, + foreground: UiColors.textWarning, + dot: UiColors.textWarning, + icon: UiIcons.swap, + ); + } + } +} + +/// Helper grouping status badge presentation values. +class ShiftCardStatusStyle { + /// Creates a [ShiftCardStatusStyle]. + const ShiftCardStatusStyle({ + required this.label, + required this.foreground, + required this.dot, + this.icon, + }); + + /// The human-readable status label. + final String label; + + /// Foreground colour for the label and icon. + final Color foreground; + + /// Dot colour when no icon is provided. + final Color dot; + + /// Optional icon replacing the dot indicator. + final IconData? icon; +} + +/// Small chip showing the order type (One Day / Multi-Day / Long Term). +class ShiftCardOrderTypeChip extends StatelessWidget { + /// Creates a [ShiftCardOrderTypeChip]. + const ShiftCardOrderTypeChip({super.key, required this.orderType}); + + /// The order type to display. + final OrderType orderType; + + @override + Widget build(BuildContext context) { + final String label = _label(context); + + return Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space2, + vertical: 2, + ), + decoration: BoxDecoration( + color: UiColors.background, + borderRadius: UiConstants.radiusSm, + border: Border.all(color: UiColors.border), + ), + child: Text( + label, + style: UiTypography.footnote2m.copyWith(color: UiColors.textSecondary), + ), + ); + } + + String _label(BuildContext context) { + switch (orderType) { + case OrderType.permanent: + return context.t.staff_shifts.filter.long_term; + case OrderType.recurring: + return context.t.staff_shifts.filter.multi_day; + case OrderType.oneTime: + case OrderType.rapid: + case OrderType.unknown: + return context.t.staff_shifts.filter.one_day; + } + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_title_row.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_title_row.dart new file mode 100644 index 00000000..f6b18b07 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_title_row.dart @@ -0,0 +1,88 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:staff_shifts/src/presentation/widgets/shift_card/shift_card_data.dart'; + +/// Title row with optional pay summary on the right. +class ShiftCardTitleRow extends StatelessWidget { + /// Creates a [ShiftCardTitleRow]. + const ShiftCardTitleRow({super.key, required this.data}); + + /// The shift data to display. + final ShiftCardData data; + + @override + Widget build(BuildContext context) { + final bool hasDirectRate = data.hourlyRate != null && data.hourlyRate! > 0; + final bool hasComputedRate = + data.hourlyRateCents != null && + data.startTime != null && + data.endTime != null; + + if (!hasDirectRate && !hasComputedRate) { + return Text( + data.title, + style: UiTypography.body2m.textPrimary, + overflow: TextOverflow.ellipsis, + ); + } + + // Prefer pre-computed values from the API when available. + final double hourlyRate; + final double estimatedTotal; + final double durationHours; + + if (hasDirectRate && data.totalRate != null && data.totalRate! > 0) { + hourlyRate = data.hourlyRate!; + estimatedTotal = data.totalRate!; + durationHours = hourlyRate > 0 ? (estimatedTotal / hourlyRate) : 0; + } else { + hourlyRate = data.hourlyRateCents! / 100; + final int durationMinutes = data.endTime! + .difference(data.startTime!) + .inMinutes; + double hours = durationMinutes / 60; + if (hours < 0) hours += 24; + durationHours = hours.roundToDouble(); + estimatedTotal = hourlyRate * durationHours; + } + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + data.title, + style: UiTypography.body2m.textPrimary, + overflow: TextOverflow.ellipsis, + ), + if (data.subtitle != null) ...[ + Text( + data.subtitle!, + style: UiTypography.body3r.textSecondary, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ), + const SizedBox(width: UiConstants.space3), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '\$${estimatedTotal.toStringAsFixed(0)}', + style: UiTypography.title1m.textPrimary, + ), + Text( + '\$${hourlyRate.toInt()}/hr \u00b7 ${durationHours.toInt()}h', + style: UiTypography.footnote2r.textSecondary, + ), + ], + ), + ], + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/history_shifts_tab.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/history_shifts_tab.dart index ebd40aa9..e6dd586f 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/history_shifts_tab.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/history_shifts_tab.dart @@ -8,7 +8,7 @@ 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.dart'; +import 'package:staff_shifts/src/presentation/widgets/shift_card/index.dart'; /// Tab displaying completed shift history. class HistoryShiftsTab extends StatelessWidget { diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/my_shifts_tab.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/my_shifts_tab.dart index f18a26ff..ceba6210 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/my_shifts_tab.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/my_shifts_tab.dart @@ -10,7 +10,7 @@ import 'package:krow_domain/krow_domain.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/widgets/shared/empty_state_view.dart'; -import 'package:staff_shifts/src/presentation/widgets/shift_card.dart'; +import 'package:staff_shifts/src/presentation/widgets/shift_card/index.dart'; /// Tab displaying the worker's assigned, pending, and cancelled shifts. class MyShiftsTab extends StatefulWidget {