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.

740 lines
27 KiB

import 'dart:convert';
3 months ago
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
3 months ago
import 'package:supplier_new/common/settings.dart';
import 'package:supplier_new/resources/tankers_model.dart';
2 months ago
// Screens (single import each) keep if you actually navigate to these
3 months ago
import 'fleet.dart';
import 'resources_drivers.dart';
import 'resources_sources.dart';
2 months ago
void main() => runApp(const MaterialApp(home: ResourcesFleetScreen()));
3 months ago
class ResourcesFleetScreen extends StatefulWidget {
const ResourcesFleetScreen({super.key});
@override
State<ResourcesFleetScreen> createState() => _ResourcesFleetScreenState();
}
class _ResourcesFleetScreenState extends State<ResourcesFleetScreen> {
final _formKey = GlobalKey<FormState>();
2 months ago
// Controllers (sheet)
final _nameCtrl = TextEditingController();
2 months ago
final _capacityCtrl = TextEditingController();
final _plateCtrl = TextEditingController();
final _mfgYearCtrl = TextEditingController();
final _insExpiryCtrl = TextEditingController();
2 months ago
// Dropdown selections (sheet)
String? selectedType;
String? selectedTypeOfWater;
// Dropdown options (adjust to your backend)
final List<String> tankerTypes = const [
"Standard Tanker",
"High-Capacity Tanker",
"Small Tanker",
];
final List<String> typeOfWater = const [
"Drinking water",
"Bore water",
];
String? _required(String? v, {String field = "This field"}) {
if (v == null || v.trim().isEmpty) return "$field is required";
return null;
}
3 months ago
int selectedTab = 0;
String search = '';
2 months ago
bool isLoading = false;
List<TankersModel> tankersList = [];
@override
void initState() {
super.initState();
_fetchTankers();
}
2 months ago
@override
void dispose() {
_nameCtrl.dispose();
_capacityCtrl.dispose();
_plateCtrl.dispose();
_mfgYearCtrl.dispose();
_insExpiryCtrl.dispose();
super.dispose();
}
Future<void> _fetchTankers() async {
setState(() => isLoading = true);
try {
final response = await AppSettings.getTankers();
final data = (jsonDecode(response)['data'] as List)
.map((e) => TankersModel.fromJson(e))
.toList();
if (!mounted) return;
setState(() {
tankersList = data;
isLoading = false;
});
} catch (e) {
2 months ago
debugPrint("⚠️ Error fetching tankers: $e");
setState(() => isLoading = false);
}
}
3 months ago
2 months ago
Future<void> _pickInsuranceDate() async {
final picked = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime(2100),
builder: (context, child) {
return Theme(
data: Theme.of(context).copyWith(
dialogBackgroundColor: Colors.white,
colorScheme: Theme.of(context).colorScheme.copyWith(
primary: const Color(0xFF8270DB),
surface: Colors.white,
onSurface: const Color(0xFF101214),
),
),
child: child!,
);
},
);
if (picked != null) {
final dd = picked.day.toString().padLeft(2, '0');
final mm = picked.month.toString().padLeft(2, '0');
final yyyy = picked.year.toString();
_insExpiryCtrl.text = "$dd-$mm-$yyyy";
setState(() {});
}
}
void _resetForm() {
_formKey.currentState?.reset();
_nameCtrl.clear();
_capacityCtrl.clear();
_plateCtrl.clear();
_mfgYearCtrl.clear();
_insExpiryCtrl.clear();
selectedType = null;
selectedTypeOfWater = null;
}
Future<void> _addTanker() async {
// Validate
final ok = _formKey.currentState?.validate() ?? false;
if (!ok) {
setState(() {}); // force rebuild to show errors
return;
}
// Build payload keys to match your backend
final payload = <String, dynamic>{
"tankerName": _nameCtrl.text.trim(),
"capacity": _capacityCtrl.text.trim(),
"typeofwater": selectedTypeOfWater ?? "",
"supplier_address": AppSettings.userAddress,
"supplier_name": AppSettings.userName,
"phoneNumber": AppSettings.phoneNumber,
"tanker_type": selectedType ?? "",
"license_plate": _plateCtrl.text.trim(),
"manufacturing_year": _mfgYearCtrl.text.trim(),
"insurance_exp_date": _insExpiryCtrl.text.trim(),
};
try {
final bool tankStatus = await AppSettings.addTankers(payload);
if (!mounted) return;
if (tankStatus) {
AppSettings.longSuccessToast("Tanker Created Successfully");
Navigator.pop(context, true); // close sheet
_resetForm();
_fetchTankers(); // refresh from server
} else {
AppSettings.longFailedToast("Tanker Creation failed");
}
} catch (e) {
debugPrint("⚠️ addTankers error: $e");
if (!mounted) return;
AppSettings.longFailedToast("Something went wrong");
}
}
3 months ago
void openTankerSimpleSheet(BuildContext context) {
2 months ago
_resetForm(); // open fresh
showModalBottomSheet(
context: context,
isScrollControlled: true,
2 months ago
backgroundColor: Colors.transparent,
builder: (context) {
2 months ago
final viewInsets = MediaQuery.of(context).viewInsets.bottom;
return FractionallySizedBox(
heightFactor: 0.75,
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Padding(
padding: EdgeInsets.fromLTRB(20, 16, 20, 20 + viewInsets),
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_LabeledField(
label: "Tanker Name *",
child: TextFormField(
controller: _nameCtrl,
validator: (v) => _required(v, field: "Tanker Name"),
decoration: InputDecoration(
hintText: "Enter Tanker Name",
hintStyle: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400),
border: const OutlineInputBorder(),
isDense: true,
),
textInputAction: TextInputAction.next,
),
),
2 months ago
_LabeledField(
label: "Tanker Capacity (in L) *",
child: TextFormField(
controller: _capacityCtrl,
validator: (v) => _required(v, field: "Tanker Capacity"),
decoration: InputDecoration(
hintText: "10,000",
hintStyle: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400),
border: const OutlineInputBorder(),
isDense: true,
),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[0-9,]')),
],
textInputAction: TextInputAction.next,
),
),
2 months ago
_LabeledField(
label: "Tanker Type *",
child: DropdownButtonFormField<String>(
value: selectedType,
items: tankerTypes
.map((t) => DropdownMenuItem(value: t, child: Text(t)))
.toList(),
onChanged: (v) => setState(() => selectedType = v),
validator: (v) => v == null || v.isEmpty ? "Tanker Type is required" : null,
isExpanded: true,
alignment: Alignment.centerLeft,
hint: Text(
"Select Type",
style: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400),
),
icon: Image.asset('images/downarrow.png', width: 16, height: 16),
decoration: const InputDecoration(
border: OutlineInputBorder(),
isDense: false,
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 14),
),
),
),
2 months ago
_LabeledField(
label: "Type of water *",
child: DropdownButtonFormField<String>(
value: selectedTypeOfWater,
items: typeOfWater
.map((t) => DropdownMenuItem(value: t, child: Text(t)))
.toList(),
onChanged: (v) => setState(() => selectedTypeOfWater = v),
validator: (v) => v == null || v.isEmpty ? "Type of water is required" : null,
isExpanded: true,
alignment: Alignment.centerLeft,
hint: Text(
"Select type of water",
style: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400),
),
icon: Image.asset('images/downarrow.png', width: 16, height: 16),
decoration: const InputDecoration(
border: OutlineInputBorder(),
isDense: false,
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 14),
),
),
),
2 months ago
_LabeledField(
label: "License Plate *",
child: TextFormField(
controller: _plateCtrl,
validator: (v) => _required(v, field: "License Plate"),
decoration: InputDecoration(
hintText: "AB 05 H 4948",
hintStyle: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400),
border: const OutlineInputBorder(),
isDense: true,
),
textCapitalization: TextCapitalization.characters,
textInputAction: TextInputAction.next,
),
),
2 months ago
_LabeledField(
label: "Manufacturing Year (opt)",
child: TextFormField(
controller: _mfgYearCtrl,
decoration: InputDecoration(
hintText: "YYYY",
hintStyle: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400),
border: const OutlineInputBorder(),
isDense: true,
),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(4),
],
textInputAction: TextInputAction.next,
),
),
2 months ago
_LabeledField(
label: "Insurance Expiry Date (opt)",
child: TextFormField(
controller: _insExpiryCtrl,
readOnly: true,
decoration: InputDecoration(
hintText: "DD-MM-YYYY",
hintStyle: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400),
border: const OutlineInputBorder(),
isDense: true,
suffixIcon: const Icon(Icons.calendar_today_outlined, size: 18),
),
onTap: _pickInsuranceDate,
),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF8270DB),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
),
onPressed: _addTanker,
child: Text(
"Save",
style: fontTextStyle(14, Colors.white, FontWeight.w600),
),
),
),
],
),
),
2 months ago
),
),
),
);
},
);
}
3 months ago
@override
Widget build(BuildContext context) {
final filtered = tankersList.where((it) {
3 months ago
final q = search.trim().toLowerCase();
if (q.isEmpty) return true;
return it.tanker_name.toLowerCase().contains(q);
3 months ago
}).toList();
return Scaffold(
backgroundColor: Colors.white,
body: Column(
children: [
2 months ago
// Header section
3 months ago
Container(
color: Colors.white,
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
child: Column(
children: [
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
2 months ago
border: Border.all(color: const Color(0xFF939495)),
3 months ago
),
child: Row(
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
color: Color(0xFFF6F0FF),
2 months ago
borderRadius: BorderRadius.all(Radius.circular(8)),
3 months ago
),
child: Padding(
padding: const EdgeInsets.all(8.0),
2 months ago
child: Image.asset('images/truck.png', fit: BoxFit.contain),
3 months ago
),
),
const SizedBox(height: 8),
2 months ago
Text('Total Tankers',
style: fontTextStyle(12, const Color(0xFF2D2E30), FontWeight.w500),
3 months ago
),
],
),
const Spacer(),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
2 months ago
children: [
Text(
tankersList.length.toString(),
style: fontTextStyle(24, const Color(0xFF0D3771), FontWeight.w500),
3 months ago
),
2 months ago
const SizedBox(height: 6),
3 months ago
Text('+2 since last month',
2 months ago
style: fontTextStyle(10, const Color(0xFF646566), FontWeight.w400),
),
3 months ago
],
),
],
),
),
const SizedBox(height: 12),
IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
2 months ago
children: const [
Expanded(child: SmallMetricBox(title: 'Active', value: '2')),
SizedBox(width: 8),
Expanded(child: SmallMetricBox(title: 'Inactive', value: '3')),
SizedBox(width: 8),
Expanded(child: SmallMetricBox(title: 'Under Maintenance', value: '1')),
3 months ago
],
),
),
],
),
),
2 months ago
// List section
3 months ago
Expanded(
child: Container(
2 months ago
color: const Color(0xFFF5F5F5),
3 months ago
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Column(
children: [
Row(
children: [
SizedBox(
width: 270,
child: Container(
2 months ago
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
3 months ago
decoration: BoxDecoration(
2 months ago
border: Border.all(color: const Color(0xFF939495), width: 0.5),
3 months ago
borderRadius: BorderRadius.circular(22),
),
child: Row(
children: [
Container(
width: 20,
height: 20,
decoration: const BoxDecoration(
image: DecorationImage(
2 months ago
image: AssetImage('images/search.png'),
fit: BoxFit.contain,
),
3 months ago
),
),
const SizedBox(width: 8),
Expanded(
child: TextField(
decoration: InputDecoration(
hintText: 'Search',
2 months ago
hintStyle: fontTextStyle(12, const Color(0xFF939495), FontWeight.w400),
3 months ago
border: InputBorder.none,
isDense: true,
),
2 months ago
onChanged: (v) => setState(() => search = v),
3 months ago
),
),
],
),
),
),
const SizedBox(width: 16),
Container(
width: 24,
height: 24,
decoration: const BoxDecoration(
image: DecorationImage(
2 months ago
image: AssetImage('images/icon_tune.png'),
fit: BoxFit.contain,
),
3 months ago
),
),
const SizedBox(width: 16),
Container(
width: 24,
height: 24,
decoration: const BoxDecoration(
image: DecorationImage(
2 months ago
image: AssetImage('images/up_down_arrow.png'),
fit: BoxFit.contain,
),
3 months ago
),
),
],
),
const SizedBox(height: 12),
Expanded(
2 months ago
child: isLoading
? const Center(child: CircularProgressIndicator())
: ListView.separated(
3 months ago
itemCount: filtered.length,
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: (context, idx) {
final it = filtered[idx];
return TankCard(
title: it.tanker_name,
subtitle: it.type_of_water,
capacity: it.capacity,
code: it.license_plate,
owner: it.supplier_name,
status: List<String>.from(it.availability),
3 months ago
);
},
),
),
],
),
),
),
),
],
),
floatingActionButton: FloatingActionButton(
2 months ago
onPressed: () => openTankerSimpleSheet(context),
3 months ago
backgroundColor: const Color(0xFF000000),
2 months ago
shape: const CircleBorder(),
child: const Icon(Icons.add, color: Colors.white),
3 months ago
),
);
}
}
// ====== SmallMetricBox ======
class SmallMetricBox extends StatelessWidget {
final String title;
final String value;
const SmallMetricBox({super.key, required this.title, required this.value});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
2 months ago
border: Border.all(color: const Color(0xFF939495)),
3 months ago
color: Colors.white,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
2 months ago
Text(
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: fontTextStyle(12, const Color(0xFF2D2E30), FontWeight.w500),
),
Text(
value,
style: fontTextStyle(24, const Color(0xFF0D3771), FontWeight.w500),
),
3 months ago
],
),
);
}
}
// ====== TankCard ======
class TankCard extends StatelessWidget {
final String title;
final String subtitle;
final String capacity;
3 months ago
final String code;
final String owner;
final List<String> status;
const TankCard({
super.key,
required this.title,
required this.subtitle,
required this.capacity,
3 months ago
required this.code,
required this.owner,
required this.status,
});
Color _chipColor(String s) {
switch (s) {
case 'filled':
return const Color(0xFFFFFFFF);
case 'available':
return const Color(0xFFE8F0FF);
case 'empty':
return const Color(0xFFFFEEEE);
case 'in-use':
return const Color(0xFFFFF0E6);
case 'maintenance':
return const Color(0xFFFFF4E6);
default:
return const Color(0xFFECECEC);
}
}
Color _chipTextColor(String s) {
switch (s) {
case 'filled':
return const Color(0xFF1D7AFC);
case 'available':
return const Color(0xFF0A9E04);
case 'empty':
return const Color(0xFFE2483D);
case 'in-use':
return const Color(0xFFEA843B);
case 'maintenance':
return const Color(0xFFD0AE3C);
default:
return Colors.black87;
}
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade200),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: status.map((s) {
final chipTextColor = _chipTextColor(s);
return Container(
margin: const EdgeInsets.only(right: 6),
2 months ago
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
3 months ago
decoration: BoxDecoration(
color: _chipColor(s),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: chipTextColor, width: 1),
),
child: Text(
2 months ago
s,
style: fontTextStyle(10, chipTextColor, FontWeight.w400),
3 months ago
),
);
}).toList(),
),
const SizedBox(height: 8),
2 months ago
Text(title, style: fontTextStyle(14, const Color(0xFF343637), FontWeight.w600)),
3 months ago
const SizedBox(height: 6),
2 months ago
Text("$subtitle - $capacity L", style: fontTextStyle(10, const Color(0xFF343637), FontWeight.w600)),
3 months ago
const SizedBox(height: 10),
Row(
children: [
ClipOval(
child: Container(
height: 12,
width: 12,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
2 months ago
image: (AppSettings.profilePictureUrl != '' && AppSettings.profilePictureUrl != 'null')
? NetworkImage(AppSettings.profilePictureUrl)
: const AssetImage("images/profile_pic.png") as ImageProvider,
fit: BoxFit.cover,
),
3 months ago
),
),
),
const SizedBox(width: 6),
Expanded(
2 months ago
child: Text(owner, style: fontTextStyle(8, const Color(0xFF646566), FontWeight.w400)),
3 months ago
),
],
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
2 months ago
Text(code, style: fontTextStyle(10, const Color(0xFF515253), FontWeight.w400)),
3 months ago
const SizedBox(height: 28),
],
),
],
),
);
}
}
2 months ago
// ====== Labeled Field Wrapper ======
class _LabeledField extends StatelessWidget {
final String label;
final Widget child;
const _LabeledField({required this.label, required this.child});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 14.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: fontTextStyle(12, const Color(0xFF515253), FontWeight.w600)),
const SizedBox(height: 6),
child,
],
),
);
}
}