Files
FTS-Portal--Unofficial-/lib/pages/audit.dart
2025-12-27 18:24:05 +00:00

643 lines
19 KiB
Dart

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