first commit

This commit is contained in:
2026-08-11 13:16:33 +05:30
parent 18531bf46b
commit c8350563d9
388 changed files with 69656 additions and 0 deletions

313
test/route_brief_test.dart Normal file
View File

@@ -0,0 +1,313 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:miler/views/Dashboard/home/route_brief.dart';
void main() {
// Two stops roughly 2.5 km and 3.5 km from a Coimbatore origin.
final stops = <String, Map<String, dynamic>>{
'A': {
'Quantity': '3',
'weight': '4.5',
'collectionamt': '250',
'pickuplat': '11.0243',
'pickuplon': '76.9558',
'kms': '2.1',
},
'B': {
'Quantity': '8',
'weight': '2',
'collectionamt': '0',
'pickuplat': '11.0312',
'pickuplon': '76.9678',
'kms': '1.5',
},
};
group('RouteBrief load & capacity', () {
test('sums parcels, weight and cash across the route', () {
final brief = RouteBrief.from(
orderIds: const ['A', 'B'],
ordersMap: stops,
shiftStart: '09:00:00',
shiftEnd: '18:00:00',
);
expect(brief.stops, 2);
expect(brief.parcels, 11);
expect(brief.weightKg, 6.5);
expect(brief.collectAmount, 250);
});
test('counts accepted stops that already left the pending list', () {
final brief = RouteBrief.from(
orderIds: const ['A'],
ordersMap: stops,
shiftStart: '',
shiftEnd: '',
extraStops: [stops['B']!],
);
// Same totals as if both were still pending — an accepted stop is still
// a stop the rider has to ride to.
expect(brief.stops, 2);
expect(brief.parcels, 11);
expect(brief.weightKg, 6.5);
});
test('strips units baked into numeric strings', () {
final brief = RouteBrief.from(
orderIds: const ['A'],
ordersMap: {
'A': {'Quantity': '2', 'weight': '3.25 kg', 'collectionamt': '₹180'},
},
shiftStart: '',
shiftEnd: '',
);
expect(brief.weightKg, 3.25);
expect(brief.collectAmount, 180);
});
});
group('RouteBrief round-trip distance', () {
test('closes the loop back to the origin', () {
final withOrigin = RouteBrief.from(
orderIds: const ['A', 'B'],
ordersMap: stops,
shiftStart: '',
shiftEnd: '',
riderLat: 11.0180,
riderLng: 76.9580,
);
final withoutOrigin = RouteBrief.from(
orderIds: const ['A', 'B'],
ordersMap: stops,
shiftStart: '',
shiftEnd: '',
);
// Origin → A → B → origin must be longer than the bare A → B leg.
expect(withOrigin.routeKm, greaterThan(withoutOrigin.routeKm));
expect(withOrigin.routeKm, greaterThan(4));
expect(withOrigin.routeKm, lessThan(15));
});
test('falls back to the per-stop kms field when coordinates are missing',
() {
final brief = RouteBrief.from(
orderIds: const ['A'],
ordersMap: {
'A': {'Quantity': '1', 'kms': '4.2'},
},
shiftStart: '',
shiftEnd: '',
);
expect(brief.routeKm, 4.2);
});
});
group('RouteBrief shift window', () {
test('flags a missing shift', () {
final brief = RouteBrief.from(
orderIds: const [],
ordersMap: const {},
shiftStart: '',
shiftEnd: '',
);
expect(brief.shiftSet, isFalse);
expect(brief.timeLeft, isNull);
expect(brief.shiftWindowLabel, 'Shift not set');
});
test('flags a half-set shift as not set', () {
final brief = RouteBrief.from(
orderIds: const [],
ordersMap: const {},
shiftStart: '09:00:00',
shiftEnd: '',
);
expect(brief.shiftSet, isFalse);
});
test('computes time left and a per-stop budget', () {
final now = DateTime(2026, 7, 27, 15, 0);
final brief = RouteBrief.from(
orderIds: const ['A', 'B'],
ordersMap: stops,
shiftStart: '09:00:00',
shiftEnd: '18:00:00',
now: now,
);
expect(brief.shiftSet, isTrue);
expect(brief.timeLeft, const Duration(hours: 3));
expect(brief.perStopBudget, const Duration(minutes: 90));
expect(brief.isPaceAtRisk, isFalse);
expect(brief.shiftWindowLabel, '9:00 AM – 6:00 PM');
});
test('clamps a finished shift to zero instead of going negative', () {
final brief = RouteBrief.from(
orderIds: const [],
ordersMap: const {},
shiftStart: '09:00:00',
shiftEnd: '18:00:00',
now: DateTime(2026, 7, 27, 20, 0),
);
expect(brief.timeLeft, Duration.zero);
expect(brief.perStopBudget, isNull);
});
test('rolls an overnight shift end into the next day', () {
final brief = RouteBrief.from(
orderIds: const [],
ordersMap: const {},
shiftStart: '22:00:00',
shiftEnd: '06:00:00',
now: DateTime(2026, 7, 27, 23, 0),
);
expect(brief.timeLeft, const Duration(hours: 7));
});
test('raises the pace flag when stops cannot fit in the time left', () {
final brief = RouteBrief.from(
orderIds: const ['A', 'B'],
ordersMap: stops,
shiftStart: '09:00:00',
shiftEnd: '18:00:00',
now: DateTime(2026, 7, 27, 17, 51), // 9 minutes for 2 stops
);
expect(brief.isPaceAtRisk, isTrue);
});
test('parses 12-hour shift strings', () {
final brief = RouteBrief.from(
orderIds: const [],
ordersMap: const {},
shiftStart: '9:30 AM',
shiftEnd: '6:00 PM',
now: DateTime(2026, 7, 27, 12, 0),
);
expect(brief.shiftSet, isTrue);
expect(brief.timeLeft, const Duration(hours: 6));
});
});
/// A missing shift is somebody else's admin: the rider cannot fix it, did not
/// cause it, and is not blocked by it — the trips still load and he can still
/// ride them. So the banner says nothing at all rather than parking a
/// permanent amber "No shift set for today" box at the top of his first
/// screen. These tests keep it that way, because a box like that comes back
/// easily and always with good intentions.
group('ShiftBanner', () {
Future<void> pump(WidgetTester tester, RouteBrief brief) async {
await tester.pumpWidget(
ScreenUtilInit(
designSize: const Size(390, 844),
builder: (_, _) => MaterialApp(
home: Scaffold(body: ShiftBanner(brief: brief)),
),
),
);
await tester.pumpAndSettle();
}
testWidgets('renders nothing at all when no shift is set', (tester) async {
await pump(
tester,
RouteBrief.from(
orderIds: const ['A', 'B'],
ordersMap: stops,
shiftStart: '',
shiftEnd: '',
),
);
expect(tester.takeException(), isNull);
expect(find.text('No shift set for today'), findsNothing);
expect(find.text('Retry'), findsNothing);
expect(find.textContaining('hub manager'), findsNothing);
// Not merely empty of those strings — the widget takes no height, so the
// route below it starts at the top of the page.
expect(tester.getSize(find.byType(ShiftBanner)).height, 0);
});
testWidgets('still shows the window once the hub sets one', (tester) async {
await pump(
tester,
RouteBrief.from(
orderIds: const ['A', 'B'],
ordersMap: stops,
shiftStart: '09:00:00',
shiftEnd: '18:00:00',
now: DateTime(2026, 7, 27, 15, 0),
),
);
expect(tester.takeException(), isNull);
expect(find.text('3h left'), findsOneWidget);
expect(tester.getSize(find.byType(ShiftBanner)).height, greaterThan(0));
});
});
group('RouteBrief time windows', () {
final now = DateTime(2026, 7, 27, 15, 0);
test('counts stops past their slot and stops closing soon', () {
final brief = RouteBrief.from(
orderIds: const ['late', 'soon', 'later'],
ordersMap: {
'late': {
'expected_pickup_time':
now.subtract(const Duration(minutes: 20)).toIso8601String(),
},
'soon': {
'expected_pickup_time':
now.add(const Duration(minutes: 10)).toIso8601String(),
},
'later': {
'expected_pickup_time':
now.add(const Duration(hours: 3)).toIso8601String(),
},
},
shiftStart: '09:00:00',
shiftEnd: '18:00:00',
now: now,
);
expect(brief.overdueStops, 1);
expect(brief.dueSoonStops, 1);
});
test('ignores stops with no slot time', () {
final brief = RouteBrief.from(
orderIds: const ['A', 'B'],
ordersMap: stops,
shiftStart: '09:00:00',
shiftEnd: '18:00:00',
now: now,
);
expect(brief.overdueStops, 0);
expect(brief.dueSoonStops, 0);
});
});
group('formatDuration', () {
test('reads like a clock', () {
expect(formatDuration(const Duration(minutes: 45)), '45m');
expect(formatDuration(const Duration(hours: 2, minutes: 15)), '2h 15m');
expect(formatDuration(const Duration(hours: 3)), '3h');
expect(formatDuration(const Duration(seconds: 20)), 'under a minute');
});
});
}