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