Files
kent_logistics_app/lib/screens/dashboard_screen.dart

583 lines
19 KiB
Dart
Raw Normal View History

2025-11-28 10:14:30 +05:30
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/auth_provider.dart';
2025-12-03 11:57:05 +05:30
import '../providers/dashboard_provider.dart';
import '../providers/mark_list_provider.dart';
import 'mark_list_screen.dart';
2025-11-28 10:14:30 +05:30
2025-12-03 11:57:05 +05:30
class DashboardScreen extends StatefulWidget {
2025-11-28 10:14:30 +05:30
const DashboardScreen({super.key});
2025-12-03 11:57:05 +05:30
@override
State<DashboardScreen> createState() => _DashboardScreenState();
}
2025-12-11 18:36:11 +05:30
class _DashboardScreenState extends State<DashboardScreen>
with TickerProviderStateMixin {
late AnimationController _scaleCtrl;
late AnimationController _shineCtrl;
2025-12-03 11:57:05 +05:30
@override
void initState() {
super.initState();
2025-12-11 18:36:11 +05:30
_scaleCtrl = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
lowerBound: 0.97,
upperBound: 1.0,
)..repeat(reverse: true);
_shineCtrl = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat();
2025-12-03 11:57:05 +05:30
WidgetsBinding.instance.addPostFrameCallback((_) async {
final auth = Provider.of<AuthProvider>(context, listen: false);
final dash = Provider.of<DashboardProvider>(context, listen: false);
dash.init(context);
await dash.loadSummary(context);
final marks = Provider.of<MarkListProvider>(context, listen: false);
marks.init(context);
await marks.loadMarks(context);
});
}
2025-12-11 18:36:11 +05:30
@override
void dispose() {
_scaleCtrl.dispose();
_shineCtrl.dispose();
super.dispose();
}
2025-12-03 11:57:05 +05:30
2025-12-11 18:36:11 +05:30
// ============================================================
// CENTERED ADD MARK POPUP
// ============================================================
void _showAddMarkForm(double scale) {
2025-12-03 11:57:05 +05:30
final markCtrl = TextEditingController();
final originCtrl = TextEditingController();
final destCtrl = TextEditingController();
2025-12-11 18:36:11 +05:30
showDialog(
2025-12-03 11:57:05 +05:30
context: context,
2025-12-11 18:36:11 +05:30
barrierDismissible: true,
2025-12-03 11:57:05 +05:30
builder: (_) {
2025-12-11 18:36:11 +05:30
return Center(
child: Material(
color: Colors.transparent,
child: Container(
width: MediaQuery.of(context).size.width * 0.88,
padding: EdgeInsets.all(20 * scale),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20 * scale),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 16,
offset: const Offset(0, 6),
),
],
2025-12-03 11:57:05 +05:30
),
2025-12-11 18:36:11 +05:30
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Add Mark No",
style: TextStyle(
fontSize: 20 * scale,
fontWeight: FontWeight.bold,
color: Colors.indigo,
),
),
SizedBox(height: 18 * scale),
_inputField(markCtrl, "Mark No", scale),
SizedBox(height: 12 * scale),
_inputField(originCtrl, "Origin", scale),
SizedBox(height: 12 * scale),
_inputField(destCtrl, "Destination", scale),
SizedBox(height: 22 * scale),
SizedBox(
width: double.infinity,
child: DecoratedBox(
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Colors.indigo, Colors.deepPurple],
),
borderRadius: BorderRadius.circular(12 * scale),
),
child: ElevatedButton(
onPressed: () async {
final mark = markCtrl.text.trim();
final origin = originCtrl.text.trim();
final dest = destCtrl.text.trim();
if (mark.isEmpty || origin.isEmpty || dest.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("All fields are required")),
);
return;
}
final provider =
Provider.of<MarkListProvider>(context, listen: false);
final res =
await provider.addMark(context, mark, origin, dest);
if (res['success'] == true) {
await Provider.of<MarkListProvider>(context,
listen: false)
.loadMarks(context);
Navigator.pop(context);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(res['message'] ?? "Failed")),
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
padding: EdgeInsets.symmetric(vertical: 14 * scale),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12 * scale),
),
),
child: Text(
"Submit",
style: TextStyle(
fontSize: 16 * scale,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
),
SizedBox(height: 6 * scale),
],
2025-12-03 11:57:05 +05:30
),
2025-12-11 18:36:11 +05:30
),
2025-12-03 11:57:05 +05:30
),
);
},
);
}
2025-12-11 18:36:11 +05:30
// ============================================================
// MAIN UI (Responsive)
// ============================================================
2025-11-28 10:14:30 +05:30
@override
Widget build(BuildContext context) {
final auth = Provider.of<AuthProvider>(context);
2025-12-03 11:57:05 +05:30
final dash = Provider.of<DashboardProvider>(context);
final marks = Provider.of<MarkListProvider>(context);
2025-12-11 18:36:11 +05:30
final name = auth.user?['customer_name'] ?? "User";
final width = MediaQuery.of(context).size.width;
final scale = (width / 430).clamp(0.88, 1.08);
2025-12-03 11:57:05 +05:30
return SingleChildScrollView(
2025-12-11 18:36:11 +05:30
padding: EdgeInsets.all(16 * scale),
2025-12-03 11:57:05 +05:30
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
2025-12-11 18:36:11 +05:30
// WELCOME CARD WITH ANIMATION
AnimatedBuilder(
animation: _scaleCtrl,
builder: (_, __) {
return Transform.scale(
scale: _scaleCtrl.value,
child: Stack(
children: [
Container(
padding: EdgeInsets.all(20 * scale),
width: double.infinity,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFFFFA726), Color(0xFFFFEB3B)],
),
borderRadius: BorderRadius.circular(18 * scale),
),
child: Text(
"Welcome, $name 👋",
style: TextStyle(
fontSize: 24 * scale,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
// Shine Animation
AnimatedBuilder(
animation: _shineCtrl,
builder: (_, __) {
final left = _shineCtrl.value *
(MediaQuery.of(context).size.width + 140) -
140;
return Positioned(
left: left,
top: -40 * scale,
bottom: -40 * scale,
child: Transform.rotate(
angle: -0.45,
child: Container(
width: 120 * scale,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.white.withOpacity(0),
Colors.white.withOpacity(.3),
Colors.white.withOpacity(0),
],
),
),
),
),
);
},
)
],
),
);
},
2025-12-03 11:57:05 +05:30
),
2025-12-11 18:36:11 +05:30
SizedBox(height: 18 * scale),
_summarySection(
dash,
rawAmount: "${dash.totalRaw ?? 0}",
scale: scale,
),
SizedBox(height: 26 * scale),
// MARK LIST CARD
Container(
padding: EdgeInsets.all(16 * scale),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14 * scale),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(.06),
blurRadius: 8 * scale,
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Add Mark Button
Center(
child: SizedBox(
width: width * 0.95,
child: DecoratedBox(
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Colors.indigo, Colors.deepPurple],
),
borderRadius: BorderRadius.circular(12 * scale),
),
child: ElevatedButton(
onPressed: () => _showAddMarkForm(scale),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
padding: EdgeInsets.symmetric(vertical: 14 * scale),
),
child: Text(
"Add Mark No",
style: TextStyle(
fontSize: 16 * scale,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
),
),
SizedBox(height: 16 * scale),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Latest Mark Numbers",
style: TextStyle(
fontSize: 18 * scale,
fontWeight: FontWeight.bold,
),
),
if (marks.marks.isNotEmpty)
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const MarkListScreen()),
);
},
child: Text(
"View All →",
style: TextStyle(
fontSize: 15 * scale,
color: Colors.indigo,
fontWeight: FontWeight.bold,
),
),
),
],
),
SizedBox(height: 12 * scale),
// Scrollable Mark List
SizedBox(
height: 300 * scale,
child: Scrollbar(
thumbVisibility: true,
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: marks.marks.length,
itemBuilder: (context, i) =>
_markTile(marks.marks[i], scale),
),
),
),
],
),
),
SizedBox(height: 40 * scale),
],
),
);
}
// ============================================================
// SUMMARY SECTION
// ============================================================
Widget _summarySection(dash,
{required String rawAmount, required double scale}) {
return Container(
padding: EdgeInsets.all(14 * scale),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14 * scale),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(.06),
blurRadius: 8 * scale,
),
],
),
child: Column(
children: [
2025-12-03 11:57:05 +05:30
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
2025-12-11 18:36:11 +05:30
_summaryTile("Active Orders", dash.activeOrders ?? 0,
Colors.blue, Icons.inventory, scale),
_summaryTile("In Transit", dash.inTransitOrders ?? 0,
Colors.orange, Icons.local_shipping, scale),
2025-12-03 11:57:05 +05:30
],
),
2025-12-11 18:36:11 +05:30
SizedBox(height: 12 * scale),
2025-12-03 11:57:05 +05:30
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
2025-12-11 18:36:11 +05:30
_summaryTile("Delivered", dash.deliveredOrders ?? 0,
Colors.green, Icons.check_circle, scale),
_summaryTile("Total Value", "${dash.totalValue ?? 0}",
Colors.teal, Icons.money, scale),
2025-12-03 11:57:05 +05:30
],
),
2025-12-11 18:36:11 +05:30
SizedBox(height: 16 * scale),
2025-12-03 11:57:05 +05:30
2025-12-11 18:36:11 +05:30
_rawAmountTile("Raw Amount", rawAmount, scale),
],
),
);
}
2025-12-03 11:57:05 +05:30
2025-12-11 18:36:11 +05:30
Widget _summaryTile(String title, dynamic value, Color color,
IconData icon, double scale) {
return Container(
width: MediaQuery.of(context).size.width * 0.41,
padding: EdgeInsets.all(14 * scale),
decoration: BoxDecoration(
color: color.withOpacity(.12),
borderRadius: BorderRadius.circular(14 * scale),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
value.toString(),
style: TextStyle(
fontSize: 20 * scale,
fontWeight: FontWeight.bold,
color: color,
),
2025-12-03 11:57:05 +05:30
),
2025-12-11 18:36:11 +05:30
SizedBox(height: 2 * scale),
SizedBox(
width: 100 * scale,
child: Text(
title,
style: TextStyle(
fontSize: 13 * scale,
fontWeight: FontWeight.w600,
),
),
),
],
),
Icon(icon, size: 28 * scale, color: color),
2025-11-28 10:14:30 +05:30
],
),
2025-12-03 11:57:05 +05:30
);
}
2025-12-11 18:36:11 +05:30
Widget _rawAmountTile(String title, String value, double scale) {
2025-12-03 11:57:05 +05:30
return Container(
2025-12-11 18:36:11 +05:30
padding: EdgeInsets.all(14 * scale),
2025-12-03 11:57:05 +05:30
decoration: BoxDecoration(
2025-12-11 18:36:11 +05:30
color: Colors.deepPurple.shade50,
borderRadius: BorderRadius.circular(14 * scale),
2025-12-03 11:57:05 +05:30
),
2025-12-11 18:36:11 +05:30
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
2025-12-03 11:57:05 +05:30
children: [
2025-12-11 18:36:11 +05:30
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
value,
style: TextStyle(
fontSize: 20 * scale,
color: Colors.deepPurple,
fontWeight: FontWeight.bold,
),
),
Text(
title,
style: TextStyle(
fontSize: 13 * scale,
color: Colors.grey,
),
),
],
),
Icon(Icons.currency_rupee,
size: 28 * scale, color: Colors.deepPurple),
2025-12-03 11:57:05 +05:30
],
),
);
}
2025-12-11 18:36:11 +05:30
// ============================================================
// MARK TILE
// ============================================================
Widget _markTile(dynamic m, double scale) {
2025-12-03 11:57:05 +05:30
return Container(
2025-12-11 18:36:11 +05:30
margin: EdgeInsets.only(bottom: 12 * scale),
padding: EdgeInsets.all(14 * scale),
2025-12-03 11:57:05 +05:30
decoration: BoxDecoration(
2025-12-11 18:36:11 +05:30
gradient: const LinearGradient(
colors: [Color(0xFF42A5F5), Color(0xFF80DEEA)],
),
borderRadius: BorderRadius.circular(14 * scale),
2025-12-03 11:57:05 +05:30
),
2025-12-11 18:36:11 +05:30
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
2025-12-03 11:57:05 +05:30
children: [
2025-12-11 18:36:11 +05:30
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
m['mark_no'],
style: TextStyle(
fontSize: 18 * scale,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
Text(
"${m['origin']}${m['destination']}",
style: TextStyle(
fontSize: 14 * scale,
color: Colors.white,
),
),
],
),
Container(
padding: EdgeInsets.symmetric(
horizontal: 10 * scale,
vertical: 6 * scale,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(.2),
borderRadius: BorderRadius.circular(8 * scale),
),
child: Text(
m['status'],
style: TextStyle(
fontSize: 12 * scale,
color: Colors.white,
fontWeight: FontWeight.bold,
),
2025-12-03 11:57:05 +05:30
),
),
],
2025-11-28 10:14:30 +05:30
),
);
}
2025-12-11 18:36:11 +05:30
// ============================================================
// INPUT FIELD
// ============================================================
Widget _inputField(TextEditingController controller, String label, double scale) {
return TextField(
controller: controller,
style: TextStyle(fontSize: 15 * scale),
decoration: InputDecoration(
labelText: label,
labelStyle: TextStyle(fontSize: 14 * scale),
filled: true,
fillColor: Colors.lightBlue.shade50,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12 * scale),
borderSide: BorderSide.none,
),
contentPadding:
EdgeInsets.symmetric(horizontal: 14 * scale, vertical: 14 * scale),
),
);
}
2025-11-28 10:14:30 +05:30
}