You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

379 lines
12 KiB

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