parent
12a77140e3
commit
f461be2c2f
@ -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<FleetStep1Page> createState() => _FleetStep1PageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FleetStep1PageState extends State<FleetStep1Page> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
// 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<String> tankerTypes = [
|
||||||
|
"Rigid Truck",
|
||||||
|
"Trailer",
|
||||||
|
"Mini Tanker",
|
||||||
|
"Hydraulic",
|
||||||
|
];
|
||||||
|
String? selectedType;
|
||||||
|
|
||||||
|
final List<String> featureOptions = [
|
||||||
|
"GPS",
|
||||||
|
"Stainless Steel",
|
||||||
|
"Partitioned",
|
||||||
|
"Food Grade",
|
||||||
|
"Top Loading",
|
||||||
|
"Bottom Loading",
|
||||||
|
];
|
||||||
|
final Set<String> selectedFeatures = {};
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
Future<void> _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<String>(
|
||||||
|
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<String> options;
|
||||||
|
final Set<String> selected;
|
||||||
|
final ValueChanged<Set<String>> onChanged;
|
||||||
|
|
||||||
|
const _MultiSelectChips({
|
||||||
|
required this.options,
|
||||||
|
required this.selected,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_MultiSelectChips> createState() => _MultiSelectChipsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MultiSelectChipsState extends State<_MultiSelectChips> {
|
||||||
|
late Set<String> _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);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue