feat(review): implement worker review functionality with rating, feedback, and issue flags
This commit is contained in:
@@ -69,6 +69,21 @@ class _CoveragePageState extends State<CoveragePage> {
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
}
|
||||
if (state.writeStatus == CoverageWriteStatus.submitted) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: context.t.client_coverage.review.success,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
}
|
||||
if (state.writeStatus == CoverageWriteStatus.submitFailure &&
|
||||
state.writeErrorMessage != null) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: translateErrorKey(state.writeErrorMessage!),
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (BuildContext context, CoverageState state) {
|
||||
final DateTime selectedDate = state.selectedDate ?? DateTime.now();
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
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';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'package:client_coverage/src/presentation/blocs/coverage_bloc.dart';
|
||||
import 'package:client_coverage/src/presentation/blocs/coverage_event.dart';
|
||||
|
||||
/// Bottom sheet modal for cancelling a late worker's assignment.
|
||||
///
|
||||
/// Collects an optional cancellation reason and dispatches a
|
||||
/// [CoverageCancelLateWorkerRequested] event to the [CoverageBloc].
|
||||
class CancelLateWorkerSheet extends StatefulWidget {
|
||||
/// Creates a [CancelLateWorkerSheet].
|
||||
const CancelLateWorkerSheet({
|
||||
required this.worker,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The assigned worker to cancel.
|
||||
final AssignedWorker worker;
|
||||
|
||||
/// Shows the cancel-late-worker bottom sheet.
|
||||
///
|
||||
/// Captures [CoverageBloc] from [context] before opening so the sheet
|
||||
/// can dispatch events without relying on an ancestor that may be
|
||||
/// deactivated.
|
||||
static void show(BuildContext context, {required AssignedWorker worker}) {
|
||||
final CoverageBloc bloc = ReadContext(context).read<CoverageBloc>();
|
||||
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(UiConstants.space4),
|
||||
),
|
||||
),
|
||||
builder: (_) => BlocProvider<CoverageBloc>.value(
|
||||
value: bloc,
|
||||
child: CancelLateWorkerSheet(worker: worker),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<CancelLateWorkerSheet> createState() => _CancelLateWorkerSheetState();
|
||||
}
|
||||
|
||||
class _CancelLateWorkerSheetState extends State<CancelLateWorkerSheet> {
|
||||
/// Controller for the optional cancellation reason text field.
|
||||
final TextEditingController _reasonController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_reasonController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TranslationsClientCoverageCancelEn l10n =
|
||||
context.t.client_coverage.cancel;
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: UiConstants.space4,
|
||||
right: UiConstants.space4,
|
||||
top: UiConstants.space3,
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom + UiConstants.space4,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
// Drag handle
|
||||
Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.border,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Header
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
UiIcons.warning,
|
||||
color: UiColors.destructive,
|
||||
size: 28,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
child: Icon(
|
||||
UiIcons.close,
|
||||
color: UiColors.textSecondary,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(l10n.title, style: UiTypography.title1b),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
l10n.subtitle,
|
||||
style: UiTypography.body2r.copyWith(color: UiColors.destructive),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Body
|
||||
Text(
|
||||
l10n.confirm_message(name: widget.worker.fullName),
|
||||
style: UiTypography.body1m,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(
|
||||
l10n.helper_text,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Reason field
|
||||
UiTextField(
|
||||
hintText: l10n.reason_placeholder,
|
||||
maxLines: 2,
|
||||
controller: _reasonController,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Action buttons
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: UiButton.secondary(
|
||||
text: l10n.keep_worker,
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => _onConfirm(context),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.destructive,
|
||||
foregroundColor: UiColors.primaryForeground,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space3,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
l10n.confirm,
|
||||
style: UiTypography.body1b.copyWith(
|
||||
color: UiColors.primaryForeground,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Dispatches the cancel event and closes the sheet.
|
||||
void _onConfirm(BuildContext context) {
|
||||
final String reason = _reasonController.text.trim();
|
||||
|
||||
ReadContext(context).read<CoverageBloc>().add(
|
||||
CoverageCancelLateWorkerRequested(
|
||||
assignmentId: widget.worker.assignmentId,
|
||||
reason: reason.isNotEmpty ? reason : null,
|
||||
),
|
||||
);
|
||||
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'package:client_coverage/src/presentation/widgets/cancel_late_worker_sheet.dart';
|
||||
import 'package:client_coverage/src/presentation/widgets/shift_header.dart';
|
||||
import 'package:client_coverage/src/presentation/widgets/worker_row.dart';
|
||||
import 'package:client_coverage/src/presentation/widgets/worker_review_sheet.dart';
|
||||
|
||||
/// List of shifts with their workers.
|
||||
///
|
||||
@@ -98,6 +100,26 @@ class CoverageShiftList extends StatelessWidget {
|
||||
worker: worker,
|
||||
shiftStartTime:
|
||||
_formatTime(shift.timeRange.startsAt),
|
||||
showRateButton:
|
||||
worker.status == AssignmentStatus.checkedIn ||
|
||||
worker.status ==
|
||||
AssignmentStatus.checkedOut ||
|
||||
worker.status ==
|
||||
AssignmentStatus.completed,
|
||||
showCancelButton:
|
||||
worker.status == AssignmentStatus.noShow ||
|
||||
worker.status ==
|
||||
AssignmentStatus.assigned ||
|
||||
worker.status ==
|
||||
AssignmentStatus.accepted,
|
||||
onRate: () => WorkerReviewSheet.show(
|
||||
context,
|
||||
worker: worker,
|
||||
),
|
||||
onCancel: () => CancelLateWorkerSheet.show(
|
||||
context,
|
||||
worker: worker,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
|
||||
@@ -9,50 +9,63 @@ class LateWorkersAlert extends StatelessWidget {
|
||||
/// Creates a [LateWorkersAlert].
|
||||
const LateWorkersAlert({
|
||||
required this.lateCount,
|
||||
this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The number of late workers.
|
||||
final int lateCount;
|
||||
|
||||
/// Optional callback invoked when the alert is tapped.
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.destructive.withValues(alpha: 0.1),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(
|
||||
color: UiColors.destructive,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
spacing: UiConstants.space4,
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.warning,
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.destructive.withValues(alpha: 0.1),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(
|
||||
color: UiColors.destructive,
|
||||
width: 0.5,
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
context.t.client_coverage.alert
|
||||
.workers_running_late(n: lateCount, count: lateCount),
|
||||
style: UiTypography.body1b.textError,
|
||||
),
|
||||
Text(
|
||||
context.t.client_coverage.alert.auto_backup_searching,
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.textError.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
spacing: UiConstants.space4,
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.warning,
|
||||
color: UiColors.destructive,
|
||||
),
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
context.t.client_coverage.alert
|
||||
.workers_running_late(n: lateCount, count: lateCount),
|
||||
style: UiTypography.body1b.textError,
|
||||
),
|
||||
Text(
|
||||
context.t.client_coverage.alert.auto_backup_searching,
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.textError.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (onTap != null)
|
||||
const Icon(
|
||||
UiIcons.chevronRight,
|
||||
size: UiConstants.space4,
|
||||
color: UiColors.destructive,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
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';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'package:client_coverage/src/presentation/blocs/coverage_bloc.dart';
|
||||
import 'package:client_coverage/src/presentation/blocs/coverage_event.dart';
|
||||
import 'package:client_coverage/src/presentation/blocs/coverage_state.dart';
|
||||
|
||||
class WorkerReviewSheet extends StatefulWidget {
|
||||
const WorkerReviewSheet({required this.worker, super.key});
|
||||
|
||||
final AssignedWorker worker;
|
||||
|
||||
static void show(BuildContext context, {required AssignedWorker worker}) {
|
||||
final CoverageBloc bloc = ReadContext(context).read<CoverageBloc>();
|
||||
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(UiConstants.space4),
|
||||
),
|
||||
),
|
||||
builder: (_) => BlocProvider<CoverageBloc>.value(
|
||||
value: bloc,
|
||||
child: WorkerReviewSheet(worker: worker),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<WorkerReviewSheet> createState() => _WorkerReviewSheetState();
|
||||
}
|
||||
|
||||
class _WorkerReviewSheetState extends State<WorkerReviewSheet> {
|
||||
int _rating = 0;
|
||||
bool _isFavorite = false;
|
||||
bool _isBlocked = false;
|
||||
final Set<ReviewIssueFlag> _selectedFlags = <ReviewIssueFlag>{};
|
||||
final TextEditingController _feedbackController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_feedbackController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TranslationsClientCoverageReviewEn l10n =
|
||||
context.t.client_coverage.review;
|
||||
|
||||
final List<String> ratingLabels = <String>[
|
||||
l10n.rating_labels.poor,
|
||||
l10n.rating_labels.fair,
|
||||
l10n.rating_labels.good,
|
||||
l10n.rating_labels.great,
|
||||
l10n.rating_labels.excellent,
|
||||
];
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.85,
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: UiConstants.space4,
|
||||
right: UiConstants.space4,
|
||||
top: UiConstants.space3,
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom + UiConstants.space4,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
_buildDragHandle(),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
_buildHeader(context, l10n),
|
||||
const SizedBox(height: UiConstants.space5),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
_buildStarRating(ratingLabels),
|
||||
const SizedBox(height: UiConstants.space5),
|
||||
_buildToggles(l10n),
|
||||
const SizedBox(height: UiConstants.space5),
|
||||
_buildIssueFlags(l10n),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
UiTextField(
|
||||
hintText: l10n.feedback_placeholder,
|
||||
maxLines: 3,
|
||||
controller: _feedbackController,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
BlocBuilder<CoverageBloc, CoverageState>(
|
||||
buildWhen: (CoverageState previous, CoverageState current) =>
|
||||
previous.writeStatus != current.writeStatus,
|
||||
builder: (BuildContext context, CoverageState state) {
|
||||
return UiButton.primary(
|
||||
text: l10n.submit,
|
||||
fullWidth: true,
|
||||
isLoading:
|
||||
state.writeStatus == CoverageWriteStatus.submitting,
|
||||
onPressed: _rating > 0 ? () => _onSubmit(context) : null,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: UiConstants.space24),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDragHandle() {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.textDisabled,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(
|
||||
BuildContext context,
|
||||
TranslationsClientCoverageReviewEn l10n,
|
||||
) {
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(widget.worker.fullName, style: UiTypography.title1b),
|
||||
Text(l10n.title, style: UiTypography.body2r.textSecondary),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(UiIcons.close),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStarRating(List<String> ratingLabels) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List<Widget>.generate(5, (int index) {
|
||||
final bool isFilled = index < _rating;
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _rating = index + 1),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space1,
|
||||
),
|
||||
child: Icon(
|
||||
UiIcons.star,
|
||||
size: UiConstants.space8,
|
||||
color: isFilled
|
||||
? UiColors.textWarning
|
||||
: UiColors.textDisabled,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
if (_rating > 0) ...<Widget>[
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(
|
||||
ratingLabels[_rating - 1],
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildToggles(TranslationsClientCoverageReviewEn l10n) {
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: _buildToggleButton(
|
||||
icon: Icons.favorite,
|
||||
label: l10n.favorite_label,
|
||||
isActive: _isFavorite,
|
||||
activeColor: const Color(0xFFE91E63),
|
||||
onTap: () => setState(() => _isFavorite = !_isFavorite),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: _buildToggleButton(
|
||||
icon: UiIcons.ban,
|
||||
label: l10n.block_label,
|
||||
isActive: _isBlocked,
|
||||
activeColor: UiColors.destructive,
|
||||
onTap: () => setState(() => _isBlocked = !_isBlocked),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildToggleButton({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required bool isActive,
|
||||
required Color activeColor,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
final Color bgColor =
|
||||
isActive ? activeColor.withAlpha(26) : UiColors.muted;
|
||||
final Color fgColor =
|
||||
isActive ? activeColor : UiColors.textDisabled;
|
||||
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
border: isActive ? Border.all(color: activeColor, width: 0.5) : null,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Icon(icon, size: UiConstants.space5, color: fgColor),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Text(
|
||||
label,
|
||||
style: UiTypography.body2r.copyWith(color: fgColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIssueFlags(TranslationsClientCoverageReviewEn l10n) {
|
||||
final Map<ReviewIssueFlag, String> flagLabels =
|
||||
<ReviewIssueFlag, String>{
|
||||
ReviewIssueFlag.late: l10n.issue_flags.late,
|
||||
ReviewIssueFlag.uniform: l10n.issue_flags.uniform,
|
||||
ReviewIssueFlag.misconduct: l10n.issue_flags.misconduct,
|
||||
ReviewIssueFlag.noShow: l10n.issue_flags.no_show,
|
||||
ReviewIssueFlag.attitude: l10n.issue_flags.attitude,
|
||||
ReviewIssueFlag.performance: l10n.issue_flags.performance,
|
||||
ReviewIssueFlag.leftEarly: l10n.issue_flags.left_early,
|
||||
};
|
||||
|
||||
return Wrap(
|
||||
spacing: UiConstants.space2,
|
||||
runSpacing: UiConstants.space2,
|
||||
children: ReviewIssueFlag.values.map((ReviewIssueFlag flag) {
|
||||
final bool isSelected = _selectedFlags.contains(flag);
|
||||
final String label = flagLabels[flag] ?? flag.value;
|
||||
|
||||
return FilterChip(
|
||||
label: Text(label),
|
||||
selected: isSelected,
|
||||
onSelected: (bool selected) {
|
||||
setState(() {
|
||||
if (selected) {
|
||||
_selectedFlags.add(flag);
|
||||
} else {
|
||||
_selectedFlags.remove(flag);
|
||||
}
|
||||
});
|
||||
},
|
||||
selectedColor: UiColors.primary,
|
||||
labelStyle: isSelected
|
||||
? UiTypography.body3r.copyWith(color: UiColors.primaryForeground)
|
||||
: UiTypography.body3r.textSecondary,
|
||||
backgroundColor: UiColors.muted,
|
||||
checkmarkColor: UiColors.primaryForeground,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(UiConstants.space5),
|
||||
),
|
||||
side: isSelected
|
||||
? const BorderSide(color: UiColors.primary)
|
||||
: BorderSide.none,
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
void _onSubmit(BuildContext context) {
|
||||
ReadContext(context).read<CoverageBloc>().add(
|
||||
CoverageSubmitReviewRequested(
|
||||
staffId: widget.worker.staffId,
|
||||
rating: _rating,
|
||||
assignmentId: widget.worker.assignmentId,
|
||||
feedback: _feedbackController.text.trim().isNotEmpty
|
||||
? _feedbackController.text.trim()
|
||||
: null,
|
||||
issueFlags: _selectedFlags.isNotEmpty
|
||||
? _selectedFlags
|
||||
.map((ReviewIssueFlag f) => f.value)
|
||||
.toList()
|
||||
: null,
|
||||
markAsFavorite: _isFavorite ? true : null,
|
||||
),
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,10 @@ class WorkerRow extends StatelessWidget {
|
||||
const WorkerRow({
|
||||
required this.worker,
|
||||
required this.shiftStartTime,
|
||||
this.showRateButton = false,
|
||||
this.showCancelButton = false,
|
||||
this.onRate,
|
||||
this.onCancel,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -19,6 +23,18 @@ class WorkerRow extends StatelessWidget {
|
||||
/// The formatted shift start time.
|
||||
final String shiftStartTime;
|
||||
|
||||
/// Whether to show the rate action button.
|
||||
final bool showRateButton;
|
||||
|
||||
/// Whether to show the cancel action button.
|
||||
final bool showCancelButton;
|
||||
|
||||
/// Callback invoked when the rate button is tapped.
|
||||
final VoidCallback? onRate;
|
||||
|
||||
/// Callback invoked when the cancel button is tapped.
|
||||
final VoidCallback? onCancel;
|
||||
|
||||
/// Formats a [DateTime] to a readable time string (h:mm a).
|
||||
String _formatCheckInTime(DateTime? time) {
|
||||
if (time == null) return '';
|
||||
@@ -214,6 +230,73 @@ class WorkerRow extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showRateButton && onRate != null)
|
||||
GestureDetector(
|
||||
onTap: onRate,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space2,
|
||||
vertical: UiConstants.space1 / 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary.withAlpha(26),
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
border: Border.all(color: UiColors.primary, width: 0.5),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: UiConstants.space1,
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.star,
|
||||
size: 12,
|
||||
color: UiColors.primary,
|
||||
),
|
||||
Text(
|
||||
l10n.actions.rate,
|
||||
style: UiTypography.footnote2b.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showCancelButton && onCancel != null)
|
||||
GestureDetector(
|
||||
onTap: onCancel,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space2,
|
||||
vertical: UiConstants.space1 / 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.destructive.withAlpha(26),
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
border: Border.all(
|
||||
color: UiColors.destructive,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: UiConstants.space1,
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.close,
|
||||
size: 12,
|
||||
color: UiColors.destructive,
|
||||
),
|
||||
Text(
|
||||
l10n.actions.cancel,
|
||||
style: UiTypography.footnote2b.copyWith(
|
||||
color: UiColors.destructive,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user