201 lines
7.9 KiB
Dart
201 lines
7.9 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
|
|
import 'package:miler/views/helpers/constants/Colorconstants.dart';
|
|
import 'package:miler/views/helpers/constants/Font_constant.dart';
|
|
import 'package:miler/views/helpers/constants/design_constants.dart';
|
|
import 'package:miler/views/helpers/widgets/miler_sheet_kit.dart';
|
|
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
/// GOING ON / OFF DUTY
|
|
///
|
|
/// One decision, asked once.
|
|
///
|
|
/// ── What this had accumulated ──
|
|
///
|
|
/// A grab handle, a tinted icon disc, a title, a subtitle, a **drawn diagram of
|
|
/// the switch moving from one state to the other**, three icon-and-text rows
|
|
/// naming the consequences, and a slide-to-confirm. Eight blocks for a question
|
|
/// with two answers — the shape of a screen assembled from good intentions one
|
|
/// at a time rather than designed.
|
|
///
|
|
/// Each piece was individually defensible and the total was not. The diagram
|
|
/// illustrated a state change the title had already stated. The consequence
|
|
/// rows were three lines of icons for facts that fit in one sentence. And the
|
|
/// slide was borrowed from the payment flows, where the app's own rule is that
|
|
/// **slides are reserved for money** — a rider ending his shift is not
|
|
/// confirming a transaction, and asking for a drag rather than a tap made the
|
|
/// commonest action of his day the most laborious.
|
|
///
|
|
/// ── What it is ──
|
|
///
|
|
/// The question, one sentence of consequence, and two answers with an obvious
|
|
/// primary. Nothing is illustrated that is also written. Going off duty is the
|
|
/// consequential direction, so it carries the brand and states the one thing
|
|
/// that genuinely catches riders out — the app will not let him off duty while
|
|
/// a booking is open — in the sentence rather than after a failed gesture.
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
class DutySheet extends StatelessWidget {
|
|
/// True when this sheet is asking to go **on** duty.
|
|
final bool goingOnline;
|
|
|
|
/// Runs on a completed slide, and owns the work: the pending-booking check,
|
|
/// the API call, the polling restart. Returns true when duty actually
|
|
/// changed.
|
|
///
|
|
/// The knob belongs to the sheet, not the caller — it springs back on every
|
|
/// outcome, and the sheet closes itself only on a true. That is why this is a
|
|
/// `Future<bool>` and not the package's controller: the controller type is
|
|
/// not exported from `slide_to_submit_button`, so naming it here would mean
|
|
/// reaching into the package's `src/`.
|
|
final Future<bool> Function() onConfirm;
|
|
|
|
const DutySheet({
|
|
super.key,
|
|
required this.goingOnline,
|
|
required this.onConfirm,
|
|
});
|
|
|
|
/// Opens the sheet. The caller keeps the toggle logic; this only presents it.
|
|
static Future<void> show(
|
|
BuildContext context, {
|
|
required bool goingOnline,
|
|
required Future<bool> Function() onConfirm,
|
|
}) {
|
|
return showMilerSheet<void>(
|
|
context,
|
|
builder: (_) => DutySheet(goingOnline: goingOnline, onConfirm: onConfirm),
|
|
);
|
|
}
|
|
|
|
/// Green to start, brand red to stop.
|
|
///
|
|
/// Green matches the duty switch's own "on" fill, so the sheet is visibly
|
|
/// about the control that opened it. Stopping is not grey: grey is what a
|
|
/// disabled control looks like, and this is the most consequential button on
|
|
/// the screen.
|
|
Color get _accent =>
|
|
goingOnline ? ColorConstants.success : ColorConstants.primary;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// ── The SafeArea used to wrap the GLASS ──
|
|
//
|
|
// So the frosted surface stopped at the top of the gesture inset and the
|
|
// brand-red home indicator area showed through beneath it — a coloured
|
|
// stripe under every duty change. The kit pads *inside* the fabric.
|
|
return MilerSheetScaffold(
|
|
padding: EdgeInsets.fromLTRB(22.w, 0, 22.w, 18.h),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
SizedBox(height: 6.h),
|
|
|
|
Text(
|
|
goingOnline ? 'Go on duty?' : 'Go off duty?',
|
|
style: TextStyle(
|
|
fontSize: 21.sp,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: -0.5,
|
|
height: 1.2,
|
|
fontFamily: FontConstants.fontFamily,
|
|
color: ColorConstants.slateText,
|
|
),
|
|
),
|
|
SizedBox(height: 8.h),
|
|
|
|
// One sentence. The off-duty version carries the fact that used
|
|
// to be discovered only after a failed slide.
|
|
//
|
|
// Both strings were blanked at some point and the sheet shipped as a
|
|
// question with two buttons and no consequence — which is the one
|
|
// thing this rewrite existed to keep. `duty_sheet_test` names all
|
|
// three facts it must contain, so they are restored to what that
|
|
// test has always asked for.
|
|
Text(
|
|
goingOnline
|
|
? 'Your office can start assigning you trips, and live tracking '
|
|
'runs while you are on duty.'
|
|
: 'Your office stops assigning you trips and live tracking pauses. '
|
|
'A stop you have already started has to be finished '
|
|
'first.',
|
|
style: TextStyle(
|
|
fontSize: 14.5.sp,
|
|
height: 1.45,
|
|
fontWeight: FontWeight.w500,
|
|
fontFamily: FontConstants.fontFamily,
|
|
color: ColorConstants.secondaryText,
|
|
),
|
|
),
|
|
SizedBox(height: 24.h),
|
|
|
|
_confirm(context),
|
|
SizedBox(height: 4.h),
|
|
_dismiss(context),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// The answer, as a button. Green to start the day, brand to end it — the
|
|
/// same two colours the duty switch itself uses, so the sheet is visibly
|
|
/// about the control that opened it.
|
|
Widget _confirm(BuildContext context) {
|
|
return SizedBox(
|
|
height: ButtonSizes.primary,
|
|
child: ElevatedButton(
|
|
onPressed: () async {
|
|
final changed = await onConfirm();
|
|
if (changed && context.mounted) Navigator.of(context).maybePop();
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: _accent,
|
|
foregroundColor: ColorConstants.onAccent,
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(ButtonSizes.radius),
|
|
),
|
|
),
|
|
child: Text(
|
|
goingOnline ? 'Go on duty' : 'Go off duty',
|
|
style: TextStyle(
|
|
fontSize: 16.sp,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: -0.3,
|
|
fontFamily: FontConstants.fontFamily,
|
|
color: ColorConstants.onAccent,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// The way out, stated as what it leaves him as rather than as "Cancel" — a
|
|
/// rider reading two buttons should not have to work out which one is the
|
|
/// no-op.
|
|
Widget _dismiss(BuildContext context) {
|
|
return SizedBox(
|
|
height: ButtonSizes.secondary,
|
|
child: TextButton(
|
|
onPressed: () => Navigator.of(context).maybePop(),
|
|
style: TextButton.styleFrom(
|
|
foregroundColor: ColorConstants.secondaryText,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(ButtonSizes.radius),
|
|
),
|
|
),
|
|
child: Text(
|
|
goingOnline ? 'Not yet' : 'Stay on duty',
|
|
style: TextStyle(
|
|
fontSize: 15.sp,
|
|
fontWeight: FontWeight.w600,
|
|
fontFamily: FontConstants.fontFamily,
|
|
color: ColorConstants.secondaryText,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|