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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:supplier_new/common/settings.dart';
import 'package:supplier_new/resources/tankers_model.dart';
// Screens (single import each) keep if you actually navigate to these
import 'fleet.dart';
import 'resources_drivers.dart';
import 'resources_sources.dart';
void main() => runApp(const MaterialApp(home: ResourcesFleetScreen()));
class ResourcesFleetScreen extends StatefulWidget {
const ResourcesFleetScreen({super.key});
@override
State<ResourcesFleetScreen> createState() => _ResourcesFleetScreenState();
}
class _ResourcesFleetScreenState extends State<ResourcesFleetScreen> {
final _formKey = GlobalKey<FormState>();
// Controllers (sheet)
final _nameCtrl = TextEditingController();
final _capacityCtrl = TextEditingController();
final _plateCtrl = TextEditingController();
final _mfgYearCtrl = TextEditingController();
final _insExpiryCtrl = TextEditingController();
// 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;
}
int selectedTab = 0;
String search = '';
bool isLoading = false;
List<TankersModel> tankersList = [];
@override
void initState() {
super.initState();
_fetchTankers();
}
@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) {
debugPrint("⚠️ Error fetching tankers: $e");
setState(() => isLoading = false);
}
}
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");
}
}
void openTankerSimpleSheet(BuildContext context) {
_resetForm(); // open fresh
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) {
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,
),
),
_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,
),
),
_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),
),
),
),
_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),
),
),
),
_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,
),
),
_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,
),
),
_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),
),
),
),
],
),
),
),
),
),
);
},
);
}
@override
Widget build(BuildContext context) {
final filtered = tankersList.where((it) {
final q = search.trim().toLowerCase();
if (q.isEmpty) return true;
return it.tanker_name.toLowerCase().contains(q);
}).toList();
return Scaffold(
backgroundColor: Colors.white,
body: Column(
children: [
// Header section
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),
border: Border.all(color: const Color(0xFF939495)),
),
child: Row(
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
color: Color(0xFFF6F0FF),
borderRadius: BorderRadius.all(Radius.circular(8)),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Image.asset('images/truck.png', fit: BoxFit.contain),
),
),
const SizedBox(height: 8),
Text('Total Tankers',
style: fontTextStyle(12, const Color(0xFF2D2E30), FontWeight.w500),
),
],
),
const Spacer(),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
Text(
tankersList.length.toString(),
style: fontTextStyle(24, const Color(0xFF0D3771), FontWeight.w500),
),
const SizedBox(height: 6),
Text('+2 since last month',
style: fontTextStyle(10, const Color(0xFF646566), FontWeight.w400),
),
],
),
],
),
),
const SizedBox(height: 12),
IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
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')),
],
),
),
],
),
),
// List section
Expanded(
child: Container(
color: const Color(0xFFF5F5F5),
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Column(
children: [
Row(
children: [
SizedBox(
width: 270,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFF939495), width: 0.5),
borderRadius: BorderRadius.circular(22),
),
child: Row(
children: [
Container(
width: 20,
height: 20,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('images/search.png'),
fit: BoxFit.contain,
),
),
),
const SizedBox(width: 8),
Expanded(
child: TextField(
decoration: InputDecoration(
hintText: 'Search',
hintStyle: fontTextStyle(12, const Color(0xFF939495), FontWeight.w400),
border: InputBorder.none,
isDense: true,
),
onChanged: (v) => setState(() => search = v),
),
),
],
),
),
),
const SizedBox(width: 16),
Container(
width: 24,
height: 24,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('images/icon_tune.png'),
fit: BoxFit.contain,
),
),
),
const SizedBox(width: 16),
Container(
width: 24,
height: 24,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('images/up_down_arrow.png'),
fit: BoxFit.contain,
),
),
),
],
),
const SizedBox(height: 12),
Expanded(
child: isLoading
? const Center(child: CircularProgressIndicator())
: ListView.separated(
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),
);
},
),
),
],
),
),
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () => openTankerSimpleSheet(context),
backgroundColor: const Color(0xFF000000),
shape: const CircleBorder(),
child: const Icon(Icons.add, color: Colors.white),
),
);
}
}
// ====== 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),
border: Border.all(color: const Color(0xFF939495)),
color: Colors.white,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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),
),
],
),
);
}
}
// ====== TankCard ======
class TankCard extends StatelessWidget {
final String title;
final String subtitle;
final String capacity;
final String code;
final String owner;
final List<String> status;
const TankCard({
super.key,
required this.title,
required this.subtitle,
required this.capacity,
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),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _chipColor(s),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: chipTextColor, width: 1),
),
child: Text(
s,
style: fontTextStyle(10, chipTextColor, FontWeight.w400),
),
);
}).toList(),
),
const SizedBox(height: 8),
Text(title, style: fontTextStyle(14, const Color(0xFF343637), FontWeight.w600)),
const SizedBox(height: 6),
Text("$subtitle - $capacity L", style: fontTextStyle(10, const Color(0xFF343637), FontWeight.w600)),
const SizedBox(height: 10),
Row(
children: [
ClipOval(
child: Container(
height: 12,
width: 12,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
image: (AppSettings.profilePictureUrl != '' && AppSettings.profilePictureUrl != 'null')
? NetworkImage(AppSettings.profilePictureUrl)
: const AssetImage("images/profile_pic.png") as ImageProvider,
fit: BoxFit.cover,
),
),
),
),
const SizedBox(width: 6),
Expanded(
child: Text(owner, style: fontTextStyle(8, const Color(0xFF646566), FontWeight.w400)),
),
],
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(code, style: fontTextStyle(10, const Color(0xFF515253), FontWeight.w400)),
const SizedBox(height: 28),
],
),
],
),
);
}
}
// ====== 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,
],
),
);
}
}