Initial Commit
This commit is contained in:
643
lib/pages/audit.dart
Normal file
643
lib/pages/audit.dart
Normal file
@@ -0,0 +1,643 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:ui';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:proto_portal/scraper.dart';
|
||||
import 'package:proto_portal/widgets/service_card.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
|
||||
class AuditPage extends StatefulWidget {
|
||||
static GoRoute route = GoRoute(
|
||||
path: "/audit",
|
||||
builder: (context, state) {
|
||||
final serviceBase64 = state.uri.queryParameters['service'] ?? '';
|
||||
final stationBase64 = state.uri.queryParameters['station'] ?? '';
|
||||
|
||||
GtiService? service;
|
||||
String? stationLabel;
|
||||
String? crs;
|
||||
|
||||
try {
|
||||
// Decode service from base64
|
||||
final serviceJson = utf8.decode(base64Decode(serviceBase64));
|
||||
service = GtiService.fromJson(jsonDecode(serviceJson));
|
||||
|
||||
// Decode station from base64
|
||||
final stationJson = utf8.decode(base64Decode(stationBase64));
|
||||
final stationData = jsonDecode(stationJson);
|
||||
stationLabel = stationData['label'];
|
||||
crs = stationData['crs'];
|
||||
} catch (e) {
|
||||
print('Error decoding params: $e');
|
||||
}
|
||||
|
||||
return AuditPage(
|
||||
service: service,
|
||||
stationLabel: stationLabel ?? '',
|
||||
crs: crs ?? '',
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
final GtiService? service;
|
||||
final String stationLabel;
|
||||
final String crs;
|
||||
|
||||
AuditPage({
|
||||
required this.service,
|
||||
required this.stationLabel,
|
||||
required this.crs,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AuditPage> createState() => _AuditPageState();
|
||||
}
|
||||
|
||||
class _AuditPageState extends State<AuditPage> {
|
||||
|
||||
// Form controllers
|
||||
final TextEditingController _vehicleRegController = TextEditingController();
|
||||
|
||||
// Form values
|
||||
int _seats1 = 5;
|
||||
int _seats2 = 1;
|
||||
int _pax1 = 0;
|
||||
int _pax2 = 0;
|
||||
String _luggage = '0';
|
||||
String _paceNotes = '1';
|
||||
CheckboxState _vehicleTracking = CheckboxState.unchecked;
|
||||
CheckboxState _destBanner = CheckboxState.checked;
|
||||
CheckboxState _callingPatternFront = CheckboxState.unchecked;
|
||||
CheckboxState _callingPatternMiddle = CheckboxState.unchecked;
|
||||
CheckboxState _callingPatternRear = CheckboxState.unchecked;
|
||||
CheckboxState _audioEquipment = CheckboxState.unchecked;
|
||||
CheckboxState _audioWorking = CheckboxState.unchecked;
|
||||
CheckboxState _wheelchair = CheckboxState.checked;
|
||||
String _exemption = 'X';
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_vehicleRegController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _saveAudit() async {
|
||||
final auth = GtiAuth.of(context);
|
||||
|
||||
print('=== AUDIT FORM DEBUG ===');
|
||||
print('Service: ${widget.service}');
|
||||
print('Service vehicleId: ${widget.service?.vehicleId}');
|
||||
print('Service auditId: ${widget.service?.auditId}');
|
||||
print('Station CRS: ${widget.crs}');
|
||||
print('======================');
|
||||
|
||||
if (widget.service == null) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Error'),
|
||||
content: Text('Service data not loaded'),
|
||||
actions: [
|
||||
PrimaryButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final success = await auth.saveAudit(
|
||||
time: widget.service!.time,
|
||||
crs: widget.crs,
|
||||
vehicleId: widget.service!.vehicleId,
|
||||
auditId: widget.service!.auditId ?? '0',
|
||||
vehicleReg: _vehicleRegController.text,
|
||||
seats: '$_seats1$_seats2',
|
||||
pax: '$_pax1$_pax2',
|
||||
luggage: _luggage,
|
||||
paceNotes: _paceNotes,
|
||||
vehicleTracking: _vehicleTracking == CheckboxState.checked ? 'Y' : 'N',
|
||||
destBanner: _destBanner == CheckboxState.checked ? 'Y' : 'N',
|
||||
callingPatternFront: _callingPatternFront == CheckboxState.checked ? 'Y' : 'N',
|
||||
callingPatternMiddle: _callingPatternMiddle == CheckboxState.checked ? 'Y' : 'N',
|
||||
callingPatternRear: _callingPatternRear == CheckboxState.checked ? 'Y' : 'N',
|
||||
audioEquipment: _audioEquipment == CheckboxState.checked ? 'Y' : 'N',
|
||||
audioWorking: _audioWorking == CheckboxState.checked ? 'Y' : 'N',
|
||||
wheelchair: _wheelchair == CheckboxState.checked ? 'Y' : 'N',
|
||||
exemption: _exemption,
|
||||
);
|
||||
|
||||
if (success) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Success'),
|
||||
content: Text('Audit saved successfully!\n\nCheck console for request details.'),
|
||||
actions: [
|
||||
PrimaryButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
GoRouter.of(context).pop();
|
||||
},
|
||||
child: Text('OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Error'),
|
||||
content: Text('Failed to save audit.\n\nCheck console for details.\n\nVehicleId: ${widget.service!.vehicleId}\nAuditId: ${widget.service!.auditId}'),
|
||||
actions: [
|
||||
PrimaryButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Error'),
|
||||
content: Text('Error saving audit: $e'),
|
||||
actions: [
|
||||
PrimaryButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double topPadding = MediaQuery.of(context).padding.top;
|
||||
double bottomPadding = MediaQuery.of(context).padding.bottom;
|
||||
|
||||
if (widget.service == null) {
|
||||
return _scaffoldBgAndTing(
|
||||
context: context,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(LucideIcons.triangleAlert, size: 48),
|
||||
SizedBox(height: 16),
|
||||
Text('Service not found').h4,
|
||||
SizedBox(height: 8),
|
||||
OutlineButton(
|
||||
onPressed: () => GoRouter.of(context).pop(),
|
||||
child: Text('Go Back'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return _scaffoldBgAndTing(
|
||||
context: context,
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: topPadding),
|
||||
|
||||
// Header
|
||||
Container(
|
||||
height: 60,
|
||||
alignment: Alignment.center,
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(width: 8),
|
||||
Button.ghost(
|
||||
onPressed: () => GoRouter.of(context).pop(),
|
||||
child: Icon(LucideIcons.arrowLeft),
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Audit Form').h4,
|
||||
Text('${widget.service?.coach} - ${widget.service?.time}').small.muted,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 48),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 8),
|
||||
|
||||
// Form Content
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: OutlinedContainer(
|
||||
child: Column(
|
||||
children: [
|
||||
|
||||
ServiceCard.fromService(
|
||||
service: widget.service!,
|
||||
),
|
||||
|
||||
// Read-only fields
|
||||
_buildReadOnlyRow('Schedule Depart Time:', widget.service?.time ?? ''),
|
||||
Divider(),
|
||||
_buildReadOnlyRow('Diagram Number:', widget.service?.coach ?? ''),
|
||||
Divider(),
|
||||
_buildReadOnlyRow('End Destination on Route:', widget.service?.destination ?? 'N/A'),
|
||||
Divider(),
|
||||
_buildReadOnlyRow('Coach Operator:', widget.service?.operator ?? ''),
|
||||
|
||||
Divider(thickness: 2),
|
||||
|
||||
// Editable fields
|
||||
_buildTextFieldRow(
|
||||
label: 'Vehicle Reg:',
|
||||
controller: _vehicleRegController,
|
||||
maxLength: 8,
|
||||
),
|
||||
|
||||
Divider(),
|
||||
|
||||
_buildTwoDropdownRow(
|
||||
label: 'Vehicle (No. Seats):',
|
||||
value1: _seats1,
|
||||
value2: _seats2,
|
||||
onChanged1: (val) => setState(() => _seats1 = val!),
|
||||
onChanged2: (val) => setState(() => _seats2 = val!),
|
||||
),
|
||||
|
||||
Divider(),
|
||||
|
||||
_buildTwoDropdownRow(
|
||||
label: 'Pax No.:',
|
||||
value1: _pax1,
|
||||
value2: _pax2,
|
||||
onChanged1: (val) => setState(() => _pax1 = val!),
|
||||
onChanged2: (val) => setState(() => _pax2 = val!),
|
||||
),
|
||||
|
||||
Divider(),
|
||||
|
||||
_buildDropdownRow(
|
||||
label: 'Luggage Hold:',
|
||||
value: _luggage,
|
||||
items: [
|
||||
('0', 'Select'),
|
||||
('1', 'OK'),
|
||||
('2', 'Nearly Full'),
|
||||
('3', 'Full'),
|
||||
],
|
||||
onChanged: (val) => setState(() => _luggage = val!),
|
||||
),
|
||||
|
||||
Divider(),
|
||||
|
||||
_buildDropdownRow(
|
||||
label: 'Vehicle (Pace Notes):',
|
||||
value: _paceNotes,
|
||||
items: [
|
||||
('1', 'Yes - FTS Supplied'),
|
||||
('2', 'Yes - Company Supplied'),
|
||||
('3', 'Yes - Sat Nav'),
|
||||
('4', 'No - Nothing'),
|
||||
],
|
||||
onChanged: (val) => setState(() => _paceNotes = val!),
|
||||
),
|
||||
|
||||
Divider(),
|
||||
|
||||
_buildCheckboxRow(
|
||||
label: 'Does the service have Vehicle Tracking:',
|
||||
state: _vehicleTracking,
|
||||
onChanged: (val) => setState(() => _vehicleTracking = val),
|
||||
),
|
||||
|
||||
Divider(),
|
||||
|
||||
_buildCheckboxRow(
|
||||
label: 'Diagram Number & Destination Banner:',
|
||||
state: _destBanner,
|
||||
onChanged: (val) => setState(() => _destBanner = val),
|
||||
),
|
||||
|
||||
Divider(),
|
||||
|
||||
_buildCheckboxRow(
|
||||
label: 'Destination & Calling Pattern (Front):',
|
||||
state: _callingPatternFront,
|
||||
onChanged: (val) => setState(() => _callingPatternFront = val),
|
||||
),
|
||||
|
||||
Divider(),
|
||||
|
||||
_buildCheckboxRow(
|
||||
label: 'Destination & Calling Pattern (Middle):',
|
||||
state: _callingPatternMiddle,
|
||||
onChanged: (val) => setState(() => _callingPatternMiddle = val),
|
||||
),
|
||||
|
||||
Divider(),
|
||||
|
||||
_buildCheckboxRow(
|
||||
label: 'Destination & Calling Pattern (Rear):',
|
||||
state: _callingPatternRear,
|
||||
onChanged: (val) => setState(() => _callingPatternRear = val),
|
||||
),
|
||||
|
||||
Divider(),
|
||||
|
||||
_buildCheckboxRow(
|
||||
label: 'Audio Announcement Equipment:',
|
||||
state: _audioEquipment,
|
||||
onChanged: (val) => setState(() => _audioEquipment = val),
|
||||
),
|
||||
|
||||
Divider(),
|
||||
|
||||
_buildCheckboxRow(
|
||||
label: 'Audio Announcement Working:',
|
||||
state: _audioWorking,
|
||||
onChanged: (val) => setState(() => _audioWorking = val),
|
||||
),
|
||||
|
||||
Divider(),
|
||||
|
||||
Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: Text(
|
||||
'Where Audio Announcement is \'No\', please instruct the driver to perform verbal announcements'
|
||||
).small.muted,
|
||||
),
|
||||
|
||||
Divider(),
|
||||
|
||||
_buildCheckboxRow(
|
||||
label: 'Wheelchair Accessible:',
|
||||
state: _wheelchair,
|
||||
onChanged: (val) => setState(() => _wheelchair = val),
|
||||
),
|
||||
|
||||
Divider(),
|
||||
|
||||
_buildDropdownRow(
|
||||
label: 'Where Wheelchair Accessible is \'No\', is an MTE Exemption certificate visible?:',
|
||||
value: _exemption,
|
||||
items: [
|
||||
('X', 'N/A'),
|
||||
('N', 'N'),
|
||||
('Y', 'Y'),
|
||||
],
|
||||
onChanged: (val) => setState(() => _exemption = val!),
|
||||
),
|
||||
|
||||
Divider(),
|
||||
|
||||
// Save button
|
||||
Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: PrimaryButton(
|
||||
onPressed: _saveAudit,
|
||||
child: Text('Save Audit'),
|
||||
).sized(width: double.infinity),
|
||||
),
|
||||
|
||||
],
|
||||
).withPadding(all: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: bottomPadding),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReadOnlyRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 7,
|
||||
child: Text(label).small,
|
||||
),
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Text(value).small,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextFieldRow({
|
||||
required String label,
|
||||
required TextEditingController controller,
|
||||
int? maxLength,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 7,
|
||||
child: Text(label).small,
|
||||
),
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
maxLength: maxLength,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTwoDropdownRow({
|
||||
required String label,
|
||||
required int value1,
|
||||
required int value2,
|
||||
required ValueChanged<int?> onChanged1,
|
||||
required ValueChanged<int?> onChanged2,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 7,
|
||||
child: Text(label).small,
|
||||
),
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Select<int>(
|
||||
itemBuilder: (context, item) => Text('$item'),
|
||||
value: value1,
|
||||
onChanged: onChanged1,
|
||||
popup: SelectPopup.noVirtualization(
|
||||
items: SelectItemList(
|
||||
children: [
|
||||
for (int i = 0; i <= 9; i++)
|
||||
SelectItemButton(value: i, child: Text('$i')),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Select<int>(
|
||||
itemBuilder: (context, item) => Text('$item'),
|
||||
value: value2,
|
||||
onChanged: onChanged2,
|
||||
popup: SelectPopup.noVirtualization(
|
||||
items: SelectItemList(
|
||||
children: [
|
||||
for (int i = 0; i <= 9; i++)
|
||||
SelectItemButton(value: i, child: Text('$i')),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDropdownRow({
|
||||
required String label,
|
||||
required String value,
|
||||
required List<(String, String)> items,
|
||||
required ValueChanged<String?> onChanged,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 7,
|
||||
child: Text(label).small,
|
||||
),
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Select<String>(
|
||||
itemBuilder: (context, item) {
|
||||
final itemData = items.firstWhere((i) => i.$1 == item);
|
||||
return Text(itemData.$2);
|
||||
},
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
popup: SelectPopup.noVirtualization(
|
||||
items: SelectItemList(
|
||||
children: [
|
||||
for (final item in items)
|
||||
SelectItemButton(value: item.$1, child: Text(item.$2)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCheckboxRow({
|
||||
required String label,
|
||||
required CheckboxState state,
|
||||
required ValueChanged<CheckboxState> onChanged,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||
child: Checkbox(
|
||||
state: state,
|
||||
onChanged: onChanged,
|
||||
trailing: Text(label).small,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _scaffoldBgAndTing({
|
||||
required BuildContext context,
|
||||
required Widget child,
|
||||
}) {
|
||||
Color bgColor = Colors.black;
|
||||
if (Theme.of(context).brightness == Brightness.light) {
|
||||
bgColor = Colors.white;
|
||||
} else {
|
||||
bgColor = Colors.black;
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage("assets/background-1.jpeg"),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaY: 10.0,
|
||||
sigmaX: 10.0,
|
||||
),
|
||||
child: Container(
|
||||
color: bgColor.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
|
||||
Positioned.fill(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.8),
|
||||
Colors.black.withOpacity(0.5),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
child,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user