Files
doormile_milderapp/lib/views/Dashboard/home/duty_sheet.dart
2026-08-28 11:13:15 +05:30

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,
),
),
),
);
}
}