diff --git a/lib/resources/fleet.dart b/lib/resources/fleet.dart new file mode 100644 index 0000000..f8e25ba --- /dev/null +++ b/lib/resources/fleet.dart @@ -0,0 +1,378 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +void main() => runApp(const MaterialApp(home: FleetStep1Page())); + +class FleetStep1Page extends StatefulWidget { + const FleetStep1Page({super.key}); + + @override + State createState() => _FleetStep1PageState(); +} + +class _FleetStep1PageState extends State { + final _formKey = GlobalKey(); + + // Controllers + final _nameCtrl = TextEditingController(); + final _capacityCtrl = TextEditingController(text: "10,000"); // hint-like + final _plateCtrl = TextEditingController(text: "AB 05 H 4948"); + final _mfgYearCtrl = TextEditingController(); + final _insExpiryCtrl = TextEditingController(); + + // Dropdowns / selections + final List tankerTypes = [ + "Rigid Truck", + "Trailer", + "Mini Tanker", + "Hydraulic", + ]; + String? selectedType; + + final List featureOptions = [ + "GPS", + "Stainless Steel", + "Partitioned", + "Food Grade", + "Top Loading", + "Bottom Loading", + ]; + final Set selectedFeatures = {}; + + // Helpers + Future _pickInsuranceDate() async { + final now = DateTime.now(); + final date = await showDatePicker( + context: context, + initialDate: now, + firstDate: DateTime(now.year - 1), + lastDate: DateTime(now.year + 10), + helpText: "Select Insurance Expiry Date", + ); + if (date != null) { + _insExpiryCtrl.text = "${date.year.toString().padLeft(4, '0')}-" + "${date.month.toString().padLeft(2, '0')}-" + "${date.day.toString().padLeft(2, '0')}"; + setState(() {}); + } + } + + String? _required(String? v, {String field = "This field"}) { + if (v == null || v.trim().isEmpty) return "$field is required"; + return null; + } + + @override + void dispose() { + _nameCtrl.dispose(); + _capacityCtrl.dispose(); + _plateCtrl.dispose(); + _mfgYearCtrl.dispose(); + _insExpiryCtrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final grey = Colors.grey.shade600; + + return Scaffold( + appBar: AppBar( + title: const Text("Complete Profile"), + centerTitle: false, + actions: const [ + Padding( + padding: EdgeInsets.only(right: 12), + child: Icon(Icons.calendar_today_outlined), + ), + Padding( + padding: EdgeInsets.only(right: 12), + child: Icon(Icons.more_vert), + ), + ], + ), + body: SafeArea( + child: Form( + key: _formKey, + child: ListView( + padding: const EdgeInsets.fromLTRB(20, 10, 20, 24), + children: [ + Text("Step 1/5", style: theme.textTheme.labelLarge?.copyWith(color: grey)), + const SizedBox(height: 16), + // FLEET header + Row( + children: [ + Icon(Icons.local_shipping_outlined, color: Colors.deepPurple.shade400), + const SizedBox(width: 8), + Text("FLEET", style: theme.textTheme.titleMedium?.copyWith(letterSpacing: 1.2)), + ], + ), + const SizedBox(height: 6), + Text( + "Details about your water tanker fleet", + style: theme.textTheme.bodySmall?.copyWith(color: grey), + ), + const SizedBox(height: 16), + + // Water Tanker card + _SectionCard( + title: "WATER TANKER #1", + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _LabeledField( + label: "Tanker Name *", + child: TextFormField( + controller: _nameCtrl, + decoration: const InputDecoration( + hintText: "Enter Tanker Name", + ), + validator: (v) => _required(v, field: "Tanker Name"), + ), + ), + _LabeledField( + label: "Tanker Capacity (in L) *", + child: TextFormField( + controller: _capacityCtrl, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'[0-9,]')), + ], + decoration: const InputDecoration( + hintText: "10,000", + suffixText: "L", + ), + validator: (v) => _required(v, field: "Capacity"), + ), + ), + _LabeledField( + label: "Tanker Type *", + child: DropdownButtonFormField( + value: selectedType, + items: tankerTypes + .map((e) => DropdownMenuItem(value: e, child: Text(e))) + .toList(), + onChanged: (v) => setState(() => selectedType = v), + decoration: const InputDecoration(hintText: "Select Type"), + validator: (v) => v == null ? "Tanker Type is required" : null, + ), + ), + _LabeledField( + label: "Tanker Features *", + child: _MultiSelectChips( + options: featureOptions, + selected: selectedFeatures, + onChanged: (s) => setState(() => selectedFeatures + ..clear() + ..addAll(s)), + ), + validator: () => + selectedFeatures.isEmpty ? "Select at least one feature" : null, + ), + _LabeledField( + label: "License Plate *", + child: TextFormField( + controller: _plateCtrl, + textCapitalization: TextCapitalization.characters, + decoration: const InputDecoration(hintText: "AB 05 H 4948"), + validator: (v) => _required(v, field: "License Plate"), + ), + ), + _LabeledField( + label: "Manufacturing Year (opt)", + child: TextFormField( + controller: _mfgYearCtrl, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(4), + ], + decoration: const InputDecoration(hintText: "YYYY"), + validator: (_) => null, // optional + ), + ), + _LabeledField( + label: "Insurance Expiry Date (opt)", + child: TextFormField( + controller: _insExpiryCtrl, + readOnly: true, + onTap: _pickInsuranceDate, + decoration: const InputDecoration( + hintText: "Select date", + suffixIcon: Icon(Icons.calendar_month_outlined), + ), + ), + ), + const SizedBox(height: 8), + FilledButton( + onPressed: () { + // manual validation for multi-select too + final chipsValid = selectedFeatures.isNotEmpty; + final valid = _formKey.currentState!.validate() && chipsValid; + setState(() {}); // to refresh potential helper text in chips + if (!valid) return; + + // Collect values + final data = { + "tanker_name": _nameCtrl.text.trim(), + "capacity_liters": _capacityCtrl.text.replaceAll(",", "").trim(), + "tanker_type": selectedType, + "features": selectedFeatures.toList(), + "license_plate": _plateCtrl.text.trim(), + "manufacturing_year": _mfgYearCtrl.text.trim().isEmpty + ? null + : _mfgYearCtrl.text.trim(), + "insurance_expiry": _insExpiryCtrl.text.trim().isEmpty + ? null + : _insExpiryCtrl.text.trim(), + }; + + // TODO: send to backend + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Saved: $data")), + ); + }, + child: const Text("Save & Continue"), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} + +/// Section card with a folding-style title bar (static in this example) +class _SectionCard extends StatelessWidget { + final String title; + final Widget child; + + const _SectionCard({required this.title, required this.child}); + + @override + Widget build(BuildContext context) { + final border = RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)); + return Card( + elevation: 0.8, + shape: border, + margin: const EdgeInsets.only(top: 12), + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 8, 12, 12), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + title, + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith(letterSpacing: .6, color: Colors.grey.shade700), + ), + ), + const Icon(Icons.expand_less, size: 18, color: Colors.grey), + ], + ), + const Divider(height: 20), + child, + ], + ), + ), + ); + } +} + +class _LabeledField extends StatelessWidget { + final String label; + final Widget child; + final String? Function()? validator; + + const _LabeledField({ + required this.label, + required this.child, + this.validator, + }); + + @override + Widget build(BuildContext context) { + final labelStyle = + Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.grey.shade800); + + // If a custom validator is provided (e.g., for multi-select), + // show helper/error text below. + final errorText = validator != null ? validator!() : null; + + return Padding( + padding: const EdgeInsets.only(bottom: 14.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: labelStyle), + const SizedBox(height: 6), + child, + if (errorText != null) + Padding( + padding: const EdgeInsets.only(top: 6), + child: Text(errorText, + style: TextStyle(color: Theme.of(context).colorScheme.error, fontSize: 12)), + ), + ], + ), + ); + } +} + +class _MultiSelectChips extends StatefulWidget { + final List options; + final Set selected; + final ValueChanged> onChanged; + + const _MultiSelectChips({ + required this.options, + required this.selected, + required this.onChanged, + }); + + @override + State<_MultiSelectChips> createState() => _MultiSelectChipsState(); +} + +class _MultiSelectChipsState extends State<_MultiSelectChips> { + late Set _local; + + @override + void initState() { + super.initState(); + _local = {...widget.selected}; + } + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 8, + runSpacing: -6, + children: [ + for (final opt in widget.options) + FilterChip( + label: Text(opt), + selected: _local.contains(opt), + onSelected: (v) { + setState(() { + if (v) { + _local.add(opt); + } else { + _local.remove(opt); + } + }); + widget.onChanged(_local); + }, + ), + ], + ); + } +}