Compare commits

...

2 Commits

Author SHA1 Message Date
divya abdar
0c1d3b8cb2 changes in order track file 2025-12-25 10:26:47 +05:30
divya abdar
d419e4ed60 minor changes 2025-12-23 11:44:56 +05:30
12 changed files with 998 additions and 838 deletions

File diff suppressed because one or more lines are too long

View File

@@ -3,6 +3,6 @@
// static const String fileBaseUrl = "http://103.248.30.24:3030/";
// }
class ApiConfig {
static const String baseUrl = "http://10.119.0.74:8000/api";
static const String fileBaseUrl = "http://10.119.0.74:8000/";
static const String baseUrl = "http://10.202.215.164:8000/api";
static const String fileBaseUrl = "http://10.202.215.164:8000/";
}

View File

@@ -6,10 +6,7 @@ class AppConfig {
static const String logoUrlEmulator = "http://10.0.2.2:8000/images/kent_logo2.png";
// For Physical Device (Replace with your actual PC local IP)
//static const String logoUrlDevice = "http://103.248.30.24:3030/images/kent_logo2.png";
// For Physical Device (Replace with your actual PC local IP)
static const String logoUrlDevice = "http://10.119.0.74:8000/images/kent_logo2.png";
static const String logoUrlDevice = "http://10.202.215.164:8000/images/kent_logo2.png";
// Which one to use?
static const String logoUrl = logoUrlDevice; // CHANGE THIS WHEN TESTING ON REAL DEVICE

View File

@@ -1,18 +1,14 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'dart:io';
import 'package:file_selector/file_selector.dart';
import '../services/chat_service.dart';
import '../services/reverb_socket_service.dart';
import '../services/dio_client.dart';
import '../providers/chat_unread_provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'dart:io';
import 'package:file_selector/file_selector.dart';
import 'chat_file_viewer.dart';
import '../widgets/chat_file_preview.dart';
import 'chat_file_viewer.dart';
class ChatScreen extends StatefulWidget {
const ChatScreen({super.key});
@@ -24,6 +20,7 @@ class ChatScreen extends StatefulWidget {
class _ChatScreenState extends State<ChatScreen> {
final TextEditingController _messageCtrl = TextEditingController();
final ScrollController _scrollCtrl = ScrollController();
Map<String, dynamic>? uploadingMessage;
late ChatService _chatService;
@@ -33,22 +30,18 @@ class _ChatScreenState extends State<ChatScreen> {
List<Map<String, dynamic>> messages = [];
bool isLoading = true;
// ============================
// INIT STATE
// ============================
@override
void initState() {
super.initState();
_chatService = ChatService(DioClient.getInstance(context));
// 🔔 Mark chat as OPEN (important)
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<ChatUnreadProvider>().setChatOpen(true);
});
_initChat();
}
String _guessMimeType(String path) {
final lower = path.toLowerCase();
if (lower.endsWith('.jpg') || lower.endsWith('.png')) return 'image/*';
@@ -63,7 +56,6 @@ class _ChatScreenState extends State<ChatScreen> {
final file = File(picked.path);
// 1⃣ Show uploading UI
setState(() {
uploadingMessage = {
'local_file': file,
@@ -72,44 +64,26 @@ class _ChatScreenState extends State<ChatScreen> {
};
});
// 2⃣ Upload (NO adding message)
await _chatService.sendFile(
ticketId!,
file,
onProgress: (progress) {
if (!mounted) return;
setState(() {
uploadingMessage!['progress'] = progress;
});
setState(() => uploadingMessage!['progress'] = progress);
},
);
// 3⃣ Remove sending bubble ONLY
if (!mounted) return;
setState(() {
uploadingMessage = null;
});
// 🚫 DO NOT add message here
// WebSocket will handle it
setState(() => uploadingMessage = null);
}
// ============================
// INIT CHAT
// ============================
Future<void> _initChat() async {
// 1⃣ Start chat
final ticketRes = await _chatService.startChat();
ticketId = ticketRes['ticket']['id'];
// 2⃣ Load messages
final msgs = await _chatService.getMessages(ticketId!);
messages = List<Map<String, dynamic>>.from(msgs);
// 3⃣ Realtime socket
await _socket.connect(
context: context,
ticketId: ticketId!,
@@ -117,20 +91,12 @@ class _ChatScreenState extends State<ChatScreen> {
final incomingClientId = msg['client_id'];
setState(() {
// 🧹 Remove local temp message with same client_id
messages.removeWhere(
(m) => m['client_id'] != null &&
m['client_id'] == incomingClientId,
);
// ✅ Add confirmed socket message
messages.removeWhere((m) => m['client_id'] == incomingClientId);
messages.add(msg);
});
_scrollToBottom();
},
onAdminMessage: () {
if (!mounted) {
context.read<ChatUnreadProvider>().increment();
@@ -138,37 +104,30 @@ class _ChatScreenState extends State<ChatScreen> {
},
);
if (!mounted) return;
setState(() => isLoading = false);
_scrollToBottom();
}
// ============================
// SCROLL
// ============================
void _scrollToBottom() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollCtrl.hasClients) {
_scrollCtrl.jumpTo(_scrollCtrl.position.maxScrollExtent);
_scrollCtrl.animateTo(
_scrollCtrl.position.maxScrollExtent,
duration: const Duration(milliseconds: 250),
curve: Curves.easeOut,
);
}
});
}
// ============================
// SEND MESSAGE
// ============================
Future<void> _sendMessage() async {
final text = _messageCtrl.text.trim();
if (text.isEmpty || ticketId == null) return;
_messageCtrl.clear();
final clientId = DateTime.now().millisecondsSinceEpoch.toString();
// 1⃣ ADD LOCAL MESSAGE IMMEDIATELY
setState(() {
messages.add({
'client_id': clientId,
@@ -182,7 +141,6 @@ class _ChatScreenState extends State<ChatScreen> {
_scrollToBottom();
// 2⃣ SEND TO SERVER
await _chatService.sendMessage(
ticketId!,
message: text,
@@ -190,28 +148,68 @@ class _ChatScreenState extends State<ChatScreen> {
);
}
// ============================
// DISPOSE
// ============================
@override
void dispose() {
// 🔕 Mark chat CLOSED
context.read<ChatUnreadProvider>().setChatOpen(false);
_socket.disconnect();
_messageCtrl.dispose();
_scrollCtrl.dispose();
super.dispose();
}
// ============================
// UI
// ============================
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Support Chat")),
appBar: PreferredSize(
preferredSize: const Size.fromHeight(56),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0xFF2196F3),
Color(0xFF1565C0),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 10,
offset: Offset(0, 3),
),
],
),
child: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: Padding(
padding: const EdgeInsets.only(left: 12),
child: CircleAvatar(
radius: 18,
backgroundColor: Colors.white,
child: Icon(
Icons.support_agent,
color: Colors.blue,
size: 20,
),
),
),
title: const Text(
"Support Chat",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
centerTitle: false,
iconTheme: const IconThemeData(color: Colors.white),
),
),
),
body: isLoading
? const Center(child: CircularProgressIndicator())
: Column(
@@ -223,100 +221,54 @@ class _ChatScreenState extends State<ChatScreen> {
);
}
// Future<void> _openUrl(String url) async {
// final uri = Uri.parse(url);
//
// if (await canLaunchUrl(uri)) {
// await launchUrl(
// uri,
// mode: LaunchMode.externalApplication,
// );
// } else {
// debugPrint("❌ Cannot launch URL: $url");
// }
// }
Widget _buildMessageContent({
String? message,
String? filePath,
String? fileType,
required bool isUser,
}) {
final textColor = isUser ? Colors.white : Colors.black;
if (filePath == null) {
return Text(message ?? '', style: TextStyle(color: textColor));
}
final url = "${DioClient.baseUrl}/storage/$filePath";
return GestureDetector(
onTap: () {
ChatFileViewer.open(
context,
url: url,
fileType: fileType ?? '',
);
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(_iconForFile(fileType), color: textColor),
const SizedBox(width: 8),
Text(
_labelForFile(fileType),
style: TextStyle(color: textColor),
),
],
),
);
}
IconData _iconForFile(String? type) {
if (type == null) return Icons.insert_drive_file;
if (type.startsWith('image/')) return Icons.image;
if (type.startsWith('video/')) return Icons.play_circle_fill;
if (type == 'application/pdf') return Icons.picture_as_pdf;
return Icons.insert_drive_file;
}
String _labelForFile(String? type) {
if (type == null) return "File";
if (type.startsWith('image/')) return "Image";
if (type.startsWith('video/')) return "Video";
if (type == 'application/pdf') return "PDF";
return "Download file";
}
Widget _buildMessages() {
return ListView(
controller: _scrollCtrl,
padding: const EdgeInsets.all(12),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
children: [
// EXISTING MESSAGES
...messages.map((msg) {
final isUser = msg['sender_type'] == 'App\\Models\\User';
return Align(
alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
alignment:
isUser ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
constraints: const BoxConstraints(maxWidth: 280),
margin: const EdgeInsets.symmetric(vertical: 6),
padding: const EdgeInsets.all(10),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isUser ? Colors.blue : Colors.grey.shade300,
borderRadius: BorderRadius.circular(12),
color: isUser ? null : Colors.white,
gradient: isUser
? LinearGradient(
colors: [
Colors.lightBlueAccent,
Colors.blue.shade700,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)
: null,
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(16),
topRight: const Radius.circular(16),
bottomLeft:
isUser ? const Radius.circular(16) : Radius.zero,
bottomRight:
isUser ? Radius.zero : const Radius.circular(16),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 6,
),
],
),
child: msg['file_path'] == null
? Text(
msg['message'] ?? '',
style: TextStyle(
color: isUser ? Colors.white : Colors.black,
color: isUser ? Colors.white : Colors.blue,
fontSize: 15,
),
)
: ChatFilePreview(
@@ -327,53 +279,26 @@ class _ChatScreenState extends State<ChatScreen> {
),
);
}),
// ⏳ UPLOADING MESSAGE
if (uploadingMessage != null)
Align(
alignment: Alignment.centerRight,
child: Container(
margin: const EdgeInsets.symmetric(vertical: 6),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ChatFilePreview(
filePath: uploadingMessage!['local_file'].path,
fileType: uploadingMessage!['file_type'],
isUser: true,
isLocal: true,
),
const SizedBox(height: 8),
LinearProgressIndicator(
value: uploadingMessage!['progress'],
backgroundColor: Colors.white24,
valueColor:
const AlwaysStoppedAnimation(Colors.white),
),
const SizedBox(height: 4),
const Text(
"Sending…",
style: TextStyle(
color: Colors.white,
fontSize: 12,
),
),
],
),
),
),
],
);
}
Widget _buildInput() {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(8),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 6,
),
],
),
child: Row(
children: [
IconButton(
@@ -384,18 +309,19 @@ class _ChatScreenState extends State<ChatScreen> {
child: TextField(
controller: _messageCtrl,
decoration: const InputDecoration(
hintText: "Type message",
hintText: "Type a message",
border: InputBorder.none,
),
),
),
IconButton(
icon: const Icon(Icons.send),
icon: const Icon(Icons.send, color: Colors.blue),
onPressed: _sendMessage,
),
],
),
),
),
);
}
}

View File

@@ -541,7 +541,22 @@ class _DashboardScreenState extends State<DashboardScreen>
vertical: 6 * scale,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(.2),
gradient: (m['status'] ?? '')
.toString()
.toLowerCase() ==
'active'
? const LinearGradient(
colors: [
Color(0xFF2ECC71), // Green
Color(0xFF16A085), // Teal Green
],
)
: const LinearGradient(
colors: [
Color(0xFFE74C3C), // Red
Color(0xFFC0392B), // Dark Red
],
),
borderRadius: BorderRadius.circular(8 * scale),
),
child: Text(
@@ -552,7 +567,7 @@ class _DashboardScreenState extends State<DashboardScreen>
fontWeight: FontWeight.bold,
),
),
),
)
],
),
);

View File

@@ -24,19 +24,16 @@ class _MarkListScreenState extends State<MarkListScreen> {
Widget build(BuildContext context) {
final marks = Provider.of<MarkListProvider>(context);
// Responsive scale factor
final screenWidth = MediaQuery.of(context).size.width;
final scale = (screenWidth / 390).clamp(0.82, 1.35);
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
centerTitle: true,
iconTheme: const IconThemeData(color: Colors.black),
title: Text(
"All Mark Numbers",
style: TextStyle(
@@ -50,14 +47,31 @@ class _MarkListScreenState extends State<MarkListScreen> {
body: marks.loading
? const Center(child: CircularProgressIndicator())
: ListView.builder(
padding: EdgeInsets.all(10 * scale), // smaller padding
padding: EdgeInsets.all(10 * scale),
itemCount: marks.marks.length,
itemBuilder: (_, i) {
final m = marks.marks[i];
final status =
(m['status'] ?? '').toString().toLowerCase();
final LinearGradient statusGradient =
status == 'active'
? const LinearGradient(
colors: [
Color(0xFF2ECC71), // Green
Color(0xFF1E8449), // Deep Emerald
],
)
: const LinearGradient(
colors: [
Color(0xFFE74C3C), // Red
Color(0xFFC0392B), // Dark Red
],
);
return Container(
margin: EdgeInsets.only(bottom: 10 * scale), // reduced margin
padding: EdgeInsets.all(12 * scale), // smaller padding
margin: EdgeInsets.only(bottom: 10 * scale),
padding: EdgeInsets.all(12 * scale),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [
@@ -67,16 +81,15 @@ class _MarkListScreenState extends State<MarkListScreen> {
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(14 * scale), // smaller radius
borderRadius: BorderRadius.circular(14 * scale),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.06),
blurRadius: 5 * scale, // smaller shadow
blurRadius: 5 * scale,
offset: Offset(0, 2 * scale),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -85,24 +98,20 @@ class _MarkListScreenState extends State<MarkListScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// MARK NUMBER
Text(
m['mark_no'],
style: TextStyle(
color: Colors.white,
fontSize: 16 * scale, // reduced font
fontSize: 16 * scale,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 3 * scale),
// ROUTE
Text(
"${m['origin']}${m['destination']}",
style: TextStyle(
color: Colors.white,
fontSize: 13 * scale, // reduced font
fontSize: 13 * scale,
fontWeight: FontWeight.w500,
),
),
@@ -110,21 +119,21 @@ class _MarkListScreenState extends State<MarkListScreen> {
),
),
// STATUS BADGE
// STATUS BADGE (GREEN / RED)
Container(
padding: EdgeInsets.symmetric(
horizontal: 10 * scale,
vertical: 5 * scale, // smaller badge
vertical: 5 * scale,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.92),
borderRadius: BorderRadius.circular(24 * scale),
gradient: statusGradient,
borderRadius: BorderRadius.circular(20 * scale),
),
child: Text(
m['status'],
style: TextStyle(
fontSize: 11.5 * scale,
color: Colors.black87,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),

View File

@@ -84,7 +84,6 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
children: [
_headerCard(scale),
// SUMMARY SECTION
_sectionHeader("Invoice Summary", Icons.receipt, s1, () {
setState(() => s1 = !s1);
}, scale),
@@ -92,12 +91,8 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
_detailRow(Icons.numbers, "Invoice No", invoice['invoice_number'], scale),
_detailRow(Icons.calendar_month, "Invoice Date", invoice['invoice_date'], scale),
_detailRow(Icons.date_range, "Due Date", invoice['due_date'], scale),
// _detailRow(Icons.payment, "Payment Method", invoice['payment_method'], scale),
// _detailRow(Icons.confirmation_number, "Reference No",
// invoice['reference_no'], scale),
], scale),
// AMOUNT SECTION
_sectionHeader("Amount Details", Icons.currency_rupee, s2, () {
setState(() => s2 = !s2);
}, scale),
@@ -109,7 +104,6 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
invoice['final_amount_with_gst'], scale),
], scale),
// CUSTOMER SECTION
_sectionHeader("Customer Details", Icons.person, s3, () {
setState(() => s3 = !s3);
}, scale),
@@ -121,7 +115,6 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
_detailRow(Icons.location_on, "Address", invoice['customer_address'], scale),
], scale),
// ITEMS SECTION
_sectionHeader("Invoice Items", Icons.shopping_cart, s4, () {
setState(() => s4 = !s4);
}, scale),
@@ -139,44 +132,63 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
// ---------------- HEADER CARD ----------------
Widget _headerCard(double scale) {
final statusColor = getInvoiceStatusColor(invoice["status"]);
return Container(
padding: EdgeInsets.all(18 * scale),
margin: EdgeInsets.only(bottom: 18 * scale),
decoration: BoxDecoration(
gradient:
LinearGradient(colors: [Colors.indigo.shade400, Colors.blue.shade600]),
gradient: LinearGradient(
colors: [Colors.indigo.shade400, Colors.blue.shade600],
),
borderRadius: BorderRadius.circular(16 * scale),
boxShadow: [
BoxShadow(
blurRadius: 10 * scale,
color: Colors.black.withOpacity(.15),
offset: Offset(0, 3 * scale))
offset: Offset(0, 3 * scale),
)
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Invoice #${invoice['invoice_number'] ?? '-'}",
Text(
"Invoice #${invoice['invoice_number'] ?? '-'}",
style: TextStyle(
fontSize: 22 * scale,
fontWeight: FontWeight.bold,
color: Colors.white)),
color: Colors.white,
),
),
SizedBox(height: 6 * scale),
Text("Date: ${invoice['invoice_date'] ?? '-'}",
style: TextStyle(color: Colors.white70, fontSize: 14 * scale)),
Text(
"Date: ${invoice['invoice_date'] ?? '-'}",
style: TextStyle(color: Colors.white70, fontSize: 14 * scale),
),
SizedBox(height: 10 * scale),
Container(
padding: EdgeInsets.symmetric(
vertical: 6 * scale, horizontal: 14 * scale),
decoration: BoxDecoration(
color: Colors.white.withOpacity(.2),
color: Colors.white, // ✅ Always white
borderRadius: BorderRadius.circular(50 * scale),
border: Border.all(
color: statusColor, // ✅ Different for each status
width: 1.4 * scale,
),
),
child: Text(
invoice["status"]?.toString() ?? "Unknown",
style: TextStyle(color: Colors.white, fontSize: 14 * scale),
(invoice["status"] ?? "Unknown").toString().toUpperCase(),
style: TextStyle(
color: statusColor, // ✅ Text color changes
fontSize: 14 * scale,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
)
],
),
);
@@ -199,11 +211,14 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
children: [
Icon(icon, color: Colors.white, size: 20 * scale),
SizedBox(width: 10 * scale),
Text(title,
Text(
title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15 * scale,
color: Colors.white)),
color: Colors.white,
),
),
const Spacer(),
AnimatedRotation(
turns: expanded ? .5 : 0,
@@ -236,7 +251,8 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
BoxShadow(
blurRadius: 8 * scale,
offset: Offset(0, 3 * scale),
color: Colors.black.withOpacity(.08)),
color: Colors.black.withOpacity(.08),
),
],
),
child: Column(children: children),
@@ -256,16 +272,19 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
Icon(icon, color: Colors.blueGrey, size: 20 * scale),
SizedBox(width: 10 * scale),
Expanded(
child: Text(label,
style: TextStyle(
color: Colors.grey.shade700, fontSize: 14 * scale)),
child: Text(
label,
style: TextStyle(color: Colors.grey.shade700, fontSize: 14 * scale),
),
),
Expanded(
child: Text(
value?.toString() ?? "N/A",
textAlign: TextAlign.end,
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 15 * scale),
fontWeight: FontWeight.bold,
fontSize: 15 * scale,
),
),
)
],
@@ -289,14 +308,14 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
BoxShadow(
blurRadius: 8 * scale,
offset: Offset(0, 3 * scale),
color: Colors.black.withOpacity(.08)),
color: Colors.black.withOpacity(.08),
),
],
border: Border.all(color: Colors.grey.shade300, width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// TITLE
Row(
children: [
Container(
@@ -313,15 +332,14 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
child: Text(
item['description'] ?? "Item",
style: TextStyle(
fontSize: 16 * scale, fontWeight: FontWeight.w600),
fontSize: 16 * scale,
fontWeight: FontWeight.w600,
),
),
),
],
),
SizedBox(height: 14 * scale),
// QTY & PRICE
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -329,10 +347,7 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
_itemBadge(Icons.currency_rupee, "Price", "$price", false, scale),
],
),
SizedBox(height: 12 * scale),
// TOTAL
Container(
padding: EdgeInsets.symmetric(
vertical: 10 * scale, horizontal: 14 * scale),
@@ -385,7 +400,8 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
color: highlight ? Colors.indigo.shade50 : Colors.grey.shade100,
border: Border.all(
color: highlight ? Colors.indigo : Colors.grey.shade300,
width: highlight ? 1.5 * scale : 1),
width: highlight ? 1.5 * scale : 1,
),
),
child: Row(
children: [
@@ -414,3 +430,22 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
);
}
}
// ---------------- STATUS COLOR HELPER ----------------
Color getInvoiceStatusColor(String? status) {
final s = (status ?? '')
.toLowerCase()
.replaceAll('_', ' ')
.replaceAll('-', ' ')
.trim();
if (s == 'paid') return Colors.green.shade600;
if (s == 'pending') return Colors.orange.shade600;
if (s == 'overdue') return Colors.red.shade600;
if (s == 'cancelled' || s == 'canceled') return Colors.grey.shade600;
if (s == 'in progress') return Colors.blue.shade600;
if (s == 'draft') return Colors.purple.shade600;
return Colors.blueGrey;
}

View File

@@ -32,68 +32,24 @@ class _OrdersScreenState extends State<OrdersScreen> {
}
final screenWidth = MediaQuery.of(context).size.width;
final scale = (screenWidth / 420).clamp(0.85, 1.15);
final scale = (screenWidth / 420).clamp(0.85, 1.1);
// FILTER ORDERS
final filteredOrders = provider.orders.where((o) {
final q = searchQuery.toLowerCase();
return o["order_id"].toString().toLowerCase().contains(q) ||
o["status"].toString().toLowerCase().contains(q) ||
o["description"].toString().toLowerCase().contains(q) ||
o["amount"].toString().toLowerCase().contains(q);
o["description"].toString().toLowerCase().contains(q);
}).toList();
return Column(
children: [
// ⭐⭐ WHITE ELEVATED SEARCH BAR ⭐⭐
Container(
margin: EdgeInsets.fromLTRB(16 * scale, 16 * scale, 16 * scale, 10 * scale),
padding: EdgeInsets.symmetric(horizontal: 14 * scale),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16 * scale),
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(0.12),
blurRadius: 10 * scale,
spreadRadius: 1 * scale,
offset: Offset(0, 4 * scale),
),
],
),
child: Row(
children: [
Icon(Icons.search,
size: 22 * scale, color: Colors.grey.shade700),
SizedBox(width: 10 * scale),
Expanded(
child: TextField(
onChanged: (value) => setState(() => searchQuery = value),
style: TextStyle(fontSize: 15 * scale),
decoration: InputDecoration(
hintText: "Search orders...",
hintStyle: TextStyle(
fontSize: 14 * scale,
color: Colors.grey.shade600,
),
border: InputBorder.none,
),
),
),
],
),
),
// LIST OF ORDERS
_searchBar(scale),
Expanded(
child: ListView.builder(
padding: EdgeInsets.all(16 * scale),
padding: EdgeInsets.all(14 * scale),
itemCount: filteredOrders.length,
itemBuilder: (context, i) {
final order = filteredOrders[i];
return _orderCard(order, scale);
return _orderCard(filteredOrders[i], scale);
},
),
),
@@ -101,123 +57,175 @@ class _OrdersScreenState extends State<OrdersScreen> {
);
}
// ORDER CARD UI
Widget _searchBar(double scale) {
return Container(
margin: EdgeInsets.fromLTRB(14 * scale, 14 * scale, 14 * scale, 8 * scale),
padding: EdgeInsets.symmetric(horizontal: 12 * scale),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14 * scale),
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(0.1),
blurRadius: 8 * scale,
offset: const Offset(0, 3),
),
],
),
child: Row(
children: [
Icon(Icons.search, size: 20 * scale, color: Colors.grey.shade700),
SizedBox(width: 8 * scale),
Expanded(
child: TextField(
onChanged: (v) => setState(() => searchQuery = v),
decoration: const InputDecoration(
hintText: "Search orders...",
border: InputBorder.none,
),
),
),
],
),
);
}
Widget _orderCard(Map<String, dynamic> o, double scale) {
final progress = getProgress(o['status']);
final badgeColor = getStatusColor(o['status']);
final percent = (progress * 100).toInt();
return Card(
elevation: 3 * scale,
elevation: 2,
margin: EdgeInsets.only(bottom: 12 * scale),
color: Colors.white,
margin: EdgeInsets.only(bottom: 16 * scale),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16 * scale),
borderRadius: BorderRadius.circular(14),
),
child: Padding(
padding: EdgeInsets.all(16 * scale),
padding: EdgeInsets.all(12 * scale), // 👈 tighter padding
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// TOP ROW
// HEADER
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Order #${o['order_id']}",
style: TextStyle(
fontSize: 18 * scale,
fontWeight: FontWeight.bold,
),
),
Container(
padding: EdgeInsets.symmetric(
horizontal: 12 * scale,
vertical: 6 * scale,
),
decoration: BoxDecoration(
color: badgeColor.withOpacity(0.15),
borderRadius: BorderRadius.circular(12 * scale),
),
child: Text(
o['status'],
style: TextStyle(
color: badgeColor,
fontWeight: FontWeight.bold,
fontSize: 13 * scale,
),
fontSize: 15 * scale, // 👈 slightly smaller
fontWeight: FontWeight.w600,
),
),
getStatusBadge(o['status'], scale),
],
),
SizedBox(height: 10 * scale),
SizedBox(height: 6 * scale),
Text(
o['description'],
style: TextStyle(fontSize: 14 * scale),
style: TextStyle(fontSize: 13 * scale),
),
SizedBox(height: 5 * scale),
SizedBox(height: 4 * scale),
Text(
"${o['amount']}",
style: TextStyle(
fontSize: 16 * scale,
fontSize: 15 * scale,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 18 * scale),
SizedBox(height: 12 * scale),
// PROGRESS HEADER
Align(
alignment: Alignment.centerRight,
child: Text(
"$percent%",
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 11 * scale,
color: Colors.grey.shade700,
),
),
),
_AnimatedProgressBar(progress: progress, scale: scale),
SizedBox(height: 18 * scale),
SizedBox(height: 6 * scale),
// BUTTONS
// PROGRESS LABELS
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Text("Shipment Ready",
style: TextStyle(fontSize: 10, color: Colors.grey)),
Text("Import Custom",
style: TextStyle(fontSize: 10, color: Colors.grey)),
Text("Delivered",
style: TextStyle(fontSize: 10, color: Colors.grey)),
],
),
SizedBox(height: 12 * scale),
// ACTIONS
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_btn(Icons.visibility, "View", Colors.green.shade800,
Colors.green.shade50, () => _openOrderDetails(o['order_id']), scale),
_btn(Icons.receipt_long, "Invoice", Colors.orange.shade800,
Colors.orange.shade50, () => _openInvoice(o['order_id']), scale),
_btn(Icons.local_shipping, "Track", Colors.blue.shade800,
Colors.blue.shade50, () => _openTrack(o['order_id']), scale),
_btn(Icons.visibility, "View",
Colors.green.shade700, Colors.green.shade50,
() => _openOrderDetails(o['order_id']), scale),
_btn(Icons.receipt_long, "Invoice",
Colors.orange.shade700, Colors.orange.shade50,
() => _openInvoice(o['order_id']), scale),
_btn(Icons.local_shipping, "Track",
Colors.blue.shade700, Colors.blue.shade50,
() => _openTrack(o['order_id']), scale),
],
)
),
],
),
),
);
}
// BUTTON UI
Widget _btn(IconData icon, String text, Color fg, Color bg,
VoidCallback onTap, double scale) {
Widget _btn(
IconData icon,
String text,
Color fg,
Color bg,
VoidCallback onTap,
double scale,
) {
return InkWell(
borderRadius: BorderRadius.circular(12 * scale),
onTap: onTap,
borderRadius: BorderRadius.circular(10),
child: Container(
constraints: BoxConstraints(
minWidth: 115 * scale, // 👈 makes button wider
minHeight: 36 * scale, // 👈 makes button taller
),
padding: EdgeInsets.symmetric(
horizontal: 20 * scale,
vertical: 12 * scale,
horizontal: 16 * scale, // slightly more horizontal space
vertical: 8 * scale,
),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(12 * scale),
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center, // 👈 centers content
children: [
Icon(icon, size: 18 * scale, color: fg),
SizedBox(width: 8 * scale),
Icon(icon, size: 16 * scale, color: fg),
SizedBox(width: 6 * scale),
Text(
text,
style: TextStyle(
color: fg,
fontWeight: FontWeight.w600,
fontSize: 14 * scale,
fontSize: 12 * scale,
),
),
],
@@ -226,30 +234,48 @@ class _OrdersScreenState extends State<OrdersScreen> {
);
}
// NAVIGATION
void _openOrderDetails(String id) {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => OrderDetailScreen(orderId: id)),
// ================= STATUS BADGE =================
Widget getStatusBadge(String? status, double scale) {
final config = statusConfig(status);
final isDomestic = (status ?? '').toLowerCase().contains("domestic");
return Container(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: config.bg,
borderRadius: BorderRadius.circular(12),
),
child: Text(
isDomestic ? "Domestic\nDistribution" : (status ?? "Unknown"),
textAlign: TextAlign.center,
maxLines: isDomestic ? 2 : 1,
style: TextStyle(
color: config.fg,
fontWeight: FontWeight.w600,
fontSize: 11,
height: 1.2,
),
),
);
}
void _openInvoice(String id) {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => OrderInvoiceScreen(orderId: id)),
);
}
void _openOrderDetails(String id) =>
Navigator.push(context,
MaterialPageRoute(builder: (_) => OrderDetailScreen(orderId: id)));
void _openTrack(String id) {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => OrderTrackScreen(orderId: id)),
);
}
void _openInvoice(String id) =>
Navigator.push(context,
MaterialPageRoute(builder: (_) => OrderInvoiceScreen(orderId: id)));
void _openTrack(String id) =>
Navigator.push(context,
MaterialPageRoute(builder: (_) => OrderTrackScreen(orderId: id)));
}
// PROGRESS BAR
// ================= PROGRESS BAR =================
class _AnimatedProgressBar extends StatelessWidget {
final double progress;
final double scale;
@@ -258,63 +284,85 @@ class _AnimatedProgressBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final maxW = constraints.maxWidth;
return LayoutBuilder(builder: (context, c) {
return Stack(
children: [
Container(
height: 10 * scale,
height: 8, // 👈 thinner bar
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(20 * scale),
borderRadius: BorderRadius.circular(20),
),
),
AnimatedContainer(
duration: const Duration(milliseconds: 650),
curve: Curves.easeInOut,
height: 10 * scale,
width: maxW * progress,
duration: const Duration(milliseconds: 600),
height: 8,
width: c.maxWidth * progress,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20 * scale),
borderRadius: BorderRadius.circular(20),
gradient: const LinearGradient(
colors: [
Color(0xFF4F8CFF),
Color(0xFF8A4DFF),
],
colors: [Color(0xFF4F8CFF), Color(0xFF8A4DFF)],
),
),
),
],
);
},
);
});
}
}
// PROGRESS VALUES
// ================= STATUS LOGIC =================
double getProgress(String? status) {
final s = (status ?? '').toLowerCase();
if (s == "pending") return 0.25;
if (s == "loading") return 0.40;
if (s == "in transit" || s == "intransit") return 0.65;
if (s == "dispatched") return 0.85;
if (s == "delivered") return 1.0;
if (s.contains("shipment ready")) return 0.05;
if (s.contains("export")) return 0.10;
if (s.contains("international")) return 0.20;
if (s.contains("arrived")) return 0.30;
if (s.contains("import")) return 0.40;
if (s.contains("warehouse")) return 0.50;
if (s.contains("domestic")) return 0.60;
if (s.contains("out for")) return 0.90;
if (s.contains("delivered")) return 1.0;
return 0.05;
}
// STATUS COLORS
Color getStatusColor(String? status) {
_StatusConfig statusConfig(String? status) {
final s = (status ?? '').toLowerCase();
if (s == "pending") return Colors.orange;
if (s == "loading") return Colors.amber.shade800;
if (s == "in transit" || s == "intransit") return Colors.red;
if (s == "dispatched") return Colors.blue.shade700;
if (s == "delivered") return Colors.green.shade700;
if (s.contains("shipment ready")) {
return _StatusConfig(Colors.blue.shade800, Colors.blue.shade50);
}
if (s.contains("export")) {
return _StatusConfig(Colors.purple.shade800, Colors.purple.shade50);
}
if (s.contains("international")) {
return _StatusConfig(Colors.red.shade800, Colors.red.shade50);
}
if (s.contains("arrived")) {
return _StatusConfig(Colors.orange.shade800, Colors.orange.shade50);
}
if (s.contains("import")) {
return _StatusConfig(Colors.teal.shade800, Colors.teal.shade50);
}
if (s.contains("warehouse")) {
return _StatusConfig(Colors.brown.shade800, Colors.brown.shade50);
}
if (s.contains("domestic")) {
return _StatusConfig(Colors.indigo.shade800, Colors.indigo.shade50);
}
if (s.contains("out for")) {
return _StatusConfig(Colors.pink.shade800, Colors.pink.shade50);
}
if (s.contains("delivered")) {
return _StatusConfig(Colors.green.shade800, Colors.green.shade50);
}
return Colors.black54;
return _StatusConfig(Colors.grey.shade800, Colors.grey.shade300);
}
class _StatusConfig {
final Color fg;
final Color bg;
_StatusConfig(this.fg, this.bg);
}

View File

@@ -21,6 +21,7 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
late final AnimationController progressController;
late final AnimationController shipController;
late final AnimationController timelineController;
@override
void initState() {
@@ -36,6 +37,11 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
duration: const Duration(milliseconds: 1600),
)..repeat(reverse: true);
timelineController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
);
WidgetsBinding.instance.addPostFrameCallback((_) => _loadData());
}
@@ -43,6 +49,7 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
void dispose() {
progressController.dispose();
shipController.dispose();
timelineController.dispose();
super.dispose();
}
@@ -56,19 +63,19 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
final service = OrderService(DioClient.getInstance(context));
final results = await Future.wait([
service.getShipment(widget.orderId).catchError((_) => {"success": false}),
service.trackOrder(widget.orderId).catchError((_) => {"success": false}),
service.getShipment(widget.orderId)
.catchError((_) => {"success": false}),
service.trackOrder(widget.orderId)
.catchError((_) => {"success": false}),
]);
final shipRes = results[0] as Map;
final trackRes = results[1] as Map;
/// ------------------- SHIPMENT DATA -------------------
shipment = shipRes["success"] == true
? Map<String, dynamic>.from(shipRes["shipment"] ?? {})
: null;
/// ------------------- TRACKING DATA -------------------
trackData = trackRes["success"] == true
? Map<String, dynamic>.from(trackRes["track"] ?? {})
: {};
@@ -82,6 +89,8 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
target,
curve: Curves.easeInOutCubic,
);
// Animate timeline
timelineController.forward(from: 0);
} catch (_) {}
}
@@ -90,9 +99,8 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
// ---------------- PROGRESS LOGIC ----------------
double _computeProgress() {
final status = (trackData["shipment_status"] ?? "")
.toString()
.toLowerCase();
final status =
(trackData["shipment_status"] ?? "").toString().toLowerCase();
if (status.contains("delivered")) return 1.0;
if (status.contains("dispatched")) return 0.85;
@@ -100,22 +108,9 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
if (status.contains("loading")) return 0.40;
if (status.contains("pending")) return 0.25;
if (_hasTimestamp("delivered_at")) return 1.0;
if (_hasTimestamp("dispatched_at")) return 0.85;
if (_hasTimestamp("in_transit_at")) return 0.65;
if (_hasTimestamp("loading_at")) return 0.40;
if (_hasTimestamp("pending_at")) return 0.25;
return 0.05;
}
bool _hasTimestamp(String key) {
final v = trackData[key];
if (v == null) return false;
final s = v.toString().trim().toLowerCase();
return s.isNotEmpty && s != "null";
}
String _fmt(dynamic v) {
if (v == null) return "-";
try {
@@ -126,6 +121,92 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
}
}
// ---------------- SHIPMENT STEPS DATA ----------------
final List<Map<String, dynamic>> _shipmentSteps = [
{
'title': 'Shipment Ready',
'status_key': 'shipment_ready',
'icon': Icons.inventory,
},
{
'title': 'Export Custom',
'status_key': 'export_custom',
'icon': Icons.account_balance,
},
{
'title': 'International Transit',
'status_key': 'international_transit',
'icon': Icons.flight,
},
{
'title': 'Arrived at India',
'status_key': 'arrived_india',
'icon': Icons.flag,
},
{
'title': 'Import Custom',
'status_key': 'import_custom',
'icon': Icons.account_balance_outlined,
},
{
'title': 'Warehouse',
'status_key': 'warehouse',
'icon': Icons.warehouse,
},
{
'title': 'Domestic Distribution',
'status_key': 'domestic_distribution',
'icon': Icons.local_shipping,
},
{
'title': 'Out for Delivery',
'status_key': 'out_for_delivery',
'icon': Icons.delivery_dining,
},
{
'title': 'Delivered',
'status_key': 'delivered',
'icon': Icons.verified,
},
];
// ---------------- STATUS MAPPING ----------------
Map<String, int> _statusToIndex = {
'shipment_ready': 0,
'export_custom': 1,
'international_transit': 2,
'arrived_india': 3,
'import_custom': 4,
'warehouse': 5,
'domestic_distribution': 6,
'out_for_delivery': 7,
'delivered': 8,
};
int _getCurrentStepIndex() {
final status = (trackData["shipment_status"] ?? "").toString().toLowerCase();
// Try to match exact status key first
for (var entry in _statusToIndex.entries) {
if (status.contains(entry.key)) {
return entry.value;
}
}
// Fallback mappings
if (status.contains("delivered")) return 8;
if (status.contains("dispatch") || status.contains("out for delivery")) return 7;
if (status.contains("distribution")) return 6;
if (status.contains("warehouse")) return 5;
if (status.contains("import")) return 4;
if (status.contains("arrived") || status.contains("india")) return 3;
if (status.contains("transit")) return 2;
if (status.contains("export")) return 1;
if (status.contains("ready") || status.contains("pending")) return 0;
return 0; // Default to first step
}
// ---------------- UI BUILD ----------------
@override
Widget build(BuildContext context) {
@@ -148,21 +229,13 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
SizedBox(height: 16 * scale),
_shipmentSummary(scale),
SizedBox(height: 16 * scale),
_shipmentTotals(scale),
SizedBox(height: 16 * scale),
_shipmentItems(scale), // USING SHIPMENT API ONLY
SizedBox(height: 20 * scale),
_trackingStatus(scale),
SizedBox(height: 16 * scale),
_progressBar(scale, width),
SizedBox(height: 16 * scale),
_detailsCard(scale),
// Replace horizontal progress bar with vertical timeline
_shipmentTimeline(scale),
],
),
),
@@ -223,7 +296,6 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
SizedBox(height: 12 * scale),
// Shipment ID
Container(
padding: EdgeInsets.all(12 * scale),
decoration: BoxDecoration(
@@ -282,142 +354,9 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
);
}
// ---------------- SHIPMENT TOTALS ----------------
Widget _shipmentTotals(double scale) {
if (shipment == null) return const SizedBox.shrink();
return Container(
padding: EdgeInsets.all(14 * scale),
decoration: _boxDecoration(scale),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Totals",
style:
TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
SizedBox(height: 10 * scale),
_twoCol("Total CTN", shipment!['total_ctn'], scale),
_twoCol("Total Qty", shipment!['total_qty'], scale),
_twoCol("Total Amount", shipment!['total_amount'], scale),
_twoCol("Total CBM", shipment!['total_cbm'], scale),
_twoCol("Total KG", shipment!['total_kg'], scale),
],
),
);
}
// ---------------- SHIPMENT ITEMS (FROM SHIPMENT API ONLY) ----------------
Widget _shipmentItems(double scale) {
if (shipment == null) return const SizedBox.shrink();
final items = (shipment!['items'] as List?) ?? [];
return Container(
padding: EdgeInsets.all(14 * scale),
decoration: _boxDecoration(scale),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Shipment Items",
style:
TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
SizedBox(height: 14 * scale),
...items.map((item) {
final orderId = item["order_id"]?.toString() ?? "-";
final qty = item["total_ttl_qty"]?.toString() ?? "-";
final cbm = item["total_ttl_cbm"]?.toString() ?? "-";
final kg = item["total_ttl_kg"]?.toString() ?? "-";
final amount = item["total_amount"]?.toString() ?? "-";
return Card(
elevation: 2,
margin: EdgeInsets.only(bottom: 12 * scale),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14 * scale),
),
child: Padding(
padding: EdgeInsets.all(14 * scale),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
Container(
padding: EdgeInsets.all(8 * scale),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(12 * scale),
),
child: Icon(Icons.inventory_2,
color: Colors.blue, size: 20 * scale),
),
SizedBox(width: 12 * scale),
Text(
"Order ID: $orderId",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15 * scale),
),
],
),
SizedBox(height: 12 * scale),
if (item["mark_no"] != null)
Text("Mark No: ${item["mark_no"]}",
style: TextStyle(fontSize: 13 * scale)),
SizedBox(height: 6 * scale),
Text("Quantity: $qty",
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14 * scale)),
Text("CBM: $cbm", style: TextStyle(fontSize: 13 * scale)),
Text("KG: $kg", style: TextStyle(fontSize: 13 * scale)),
SizedBox(height: 10 * scale),
Container(
padding: EdgeInsets.symmetric(
vertical: 8 * scale, horizontal: 12 * scale),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(10 * scale),
border: Border.all(color: Colors.blue, width: 1),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Icon(Icons.currency_rupee,
color: Colors.blue),
Text(
"Amount: ₹$amount",
style: TextStyle(
fontSize: 15 * scale,
fontWeight: FontWeight.bold,
color: Colors.blue),
),
],
),
),
],
),
),
);
}).toList(),
],
),
);
}
// ---------------- TRACKING STATUS ----------------
Widget _trackingStatus(double scale) {
final p = _computeProgress();
final delivered = p >= 1.0;
final delivered = _computeProgress() >= 1.0;
return Container(
padding:
@@ -438,9 +377,7 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
),
SizedBox(width: 8 * scale),
Text(
(trackData["shipment_status"] ?? "-")
.toString()
.toUpperCase(),
(trackData["shipment_status"] ?? "-").toString().toUpperCase(),
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
@@ -451,129 +388,282 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
);
}
// ---------------- PROGRESS BAR ----------------
Widget _progressBar(double scale, double width) {
final usableWidth = width - (48 * scale);
// ---------------- VERTICAL SHIPMENT TIMELINE ----------------
Widget _shipmentTimeline(double scale) {
final currentStepIndex = _getCurrentStepIndex();
return Container(
padding: EdgeInsets.all(12 * scale),
padding: EdgeInsets.all(16 * scale),
decoration: _boxDecoration(scale),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Shipment Progress",
style:
TextStyle(fontWeight: FontWeight.bold, fontSize: 15 * scale)),
SizedBox(height: 12 * scale),
Stack(
alignment: Alignment.centerLeft,
Row(
children: [
// Background
Container(
width: usableWidth,
height: 10 * scale,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(20 * scale)),
Icon(Icons.timeline, color: Colors.blue, size: 20 * scale),
SizedBox(width: 8 * scale),
Text(
"Shipment Progress",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 17 * scale,
),
),
],
),
SizedBox(height: 8 * scale),
Text(
"Tracking your package in real-time",
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 13 * scale,
),
),
SizedBox(height: 16 * scale),
// Progress Bar
// Timeline Container
AnimatedBuilder(
animation: progressController,
builder: (_, __) {
final w = usableWidth *
progressController.value.clamp(0.0, 1.0);
return Container(
width: w,
height: 10 * scale,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF4F8CFF), Color(0xFF8A4DFF)]),
borderRadius: BorderRadius.circular(20 * scale),
animation: timelineController,
builder: (context, child) {
return Opacity(
opacity: timelineController.value,
child: Transform.translate(
offset: Offset(0, 20 * (1 - timelineController.value)),
child: child,
),
);
},
child: _buildTimeline(scale, currentStepIndex),
),
],
),
);
}
Widget _buildTimeline(double scale, int currentStepIndex) {
return Container(
padding: EdgeInsets.only(left: 8 * scale),
child: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _shipmentSteps.length,
itemBuilder: (context, index) {
final step = _shipmentSteps[index];
final isCompleted = index < currentStepIndex;
final isCurrent = index == currentStepIndex;
final isLast = index == _shipmentSteps.length - 1;
return _buildTimelineStep(
scale: scale,
step: step,
index: index,
isCompleted: isCompleted,
isCurrent: isCurrent,
isLast: isLast,
currentStepIndex: currentStepIndex,
);
},
),
);
}
Widget _buildTimelineStep({
required double scale,
required Map<String, dynamic> step,
required int index,
required bool isCompleted,
required bool isCurrent,
required bool isLast,
required int currentStepIndex,
}) {
return IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Timeline Column (Line + Icon)
Column(
children: [
// Top connector line (except for first item)
if (index > 0)
Container(
width: 2 * scale,
height: 24 * scale,
color: isCompleted ? Colors.green : Colors.grey.shade300,
),
// Moving Truck Icon
AnimatedBuilder(
animation: shipController,
builder: (_, __) {
final progress = progressController.value.clamp(0.0, 1.0);
final bob = sin(shipController.value * 2 * pi) * (4 * scale);
// Step Icon with animation
_buildStepIcon(scale, isCompleted, isCurrent, step['icon']),
return Positioned(
left: (usableWidth - 26 * scale) * progress,
top: -6 * scale + bob,
// Bottom connector line (except for last item)
if (!isLast)
Container(
width: 2 * scale,
height: 24 * scale,
color: index < currentStepIndex ? Colors.green : Colors.grey.shade300,
),
],
),
SizedBox(width: 16 * scale),
// Step Content
Expanded(
child: Container(
width: 26 * scale,
height: 26 * scale,
margin: EdgeInsets.only(bottom: 20 * scale),
padding: EdgeInsets.all(14 * scale),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF4F8CFF), Color(0xFF8A4DFF)],
color: isCurrent ? Colors.blue.shade50 : Colors.transparent,
borderRadius: BorderRadius.circular(12 * scale),
border: isCurrent
? Border.all(color: Colors.blue.shade200, width: 1.5 * scale)
: null,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
step['title'],
style: TextStyle(
fontSize: 15 * scale,
fontWeight: FontWeight.w600,
color: isCurrent
? Colors.blue.shade800
: isCompleted
? Colors.green.shade800
: Colors.grey.shade800,
),
),
),
if (isCompleted)
Icon(Icons.check_circle,
color: Colors.green,
size: 18 * scale
),
],
),
SizedBox(height: 4 * scale),
// Optional timestamp - you can customize this with actual timestamps from API
_buildStepTimestamp(scale, step, isCompleted, isCurrent, index),
],
),
),
),
],
),
);
}
Widget _buildStepIcon(double scale, bool isCompleted, bool isCurrent, IconData iconData) {
// Animation for current step
if (isCurrent) {
return AnimatedBuilder(
animation: shipController,
builder: (context, child) {
return Container(
width: 32 * scale * (1 + 0.2 * shipController.value),
height: 32 * scale * (1 + 0.2 * shipController.value),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.blue.shade50,
border: Border.all(
color: Colors.blue,
width: 2 * scale,
),
borderRadius: BorderRadius.circular(8 * scale),
),
child: Icon(
Icons.local_shipping,
size: 16 * scale,
color: Colors.white,
),
iconData,
color: Colors.blue,
size: 18 * scale,
),
);
},
),
],
),
SizedBox(height: 10 * scale),
AnimatedBuilder(
animation: progressController,
builder: (_, __) => Text(
"${(progressController.value * 100).toInt()}% Completed",
style: TextStyle(color: Colors.black54, fontSize: 13 * scale),
),
)
],
),
);
}
// ---------------- DETAILS CARD ----------------
Widget _detailsCard(double scale) {
// Completed step
if (isCompleted) {
return Container(
padding: EdgeInsets.all(14 * scale),
decoration: _boxDecoration(scale),
child: Column(
children: [
_detailsRow("Order ID", trackData["order_id"], scale),
SizedBox(height: 10 * scale),
_detailsRow("Status", trackData["shipment_status"], scale),
SizedBox(height: 10 * scale),
_detailsRow("Date", _fmt(trackData["shipment_date"]), scale),
],
width: 32 * scale,
height: 32 * scale,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.green.shade50,
border: Border.all(
color: Colors.green,
width: 2 * scale,
),
),
child: Icon(
Icons.check,
color: Colors.green,
size: 18 * scale,
),
);
}
Widget _detailsRow(String title, dynamic value, double scale) {
// Upcoming step
return Container(
width: 32 * scale,
height: 32 * scale,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey.shade100,
border: Border.all(
color: Colors.grey.shade400,
width: 2 * scale,
),
),
child: Icon(
iconData,
color: Colors.grey.shade600,
size: 18 * scale,
),
);
}
Widget _buildStepTimestamp(double scale, Map<String, dynamic> step,
bool isCompleted, bool isCurrent, int index) {
// You can replace this with actual timestamps from your API
final now = DateTime.now();
final estimatedTime = now.add(Duration(days: index));
String statusText = isCurrent
? "In progress"
: isCompleted
? "Completed"
: "Estimated ${estimatedTime.day}/${estimatedTime.month}";
Color statusColor = isCurrent
? Colors.blue.shade600
: isCompleted
? Colors.green.shade600
: Colors.grey.shade600;
return Row(
children: [
Expanded(
child: Text(title,
Icon(
isCurrent ? Icons.access_time : Icons.calendar_today,
color: statusColor,
size: 14 * scale,
),
SizedBox(width: 4 * scale),
Text(
statusText,
style: TextStyle(
fontWeight: FontWeight.w700, fontSize: 14 * scale))),
Expanded(
child: Text(value?.toString() ?? "-",
textAlign: TextAlign.right,
style: TextStyle(color: Colors.black87, fontSize: 14 * scale))),
fontSize: 12 * scale,
color: statusColor,
fontWeight: FontWeight.w500,
),
),
],
);
}
// ---------------- BOX DECORATION ----------------
BoxDecoration _boxDecoration(double scale) {
return BoxDecoration(
color: Colors.white,

View File

@@ -16,7 +16,6 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
bool s1 = true;
bool s2 = false;
bool s3 = false;
bool s4 = false;
@override
void initState() {
@@ -27,8 +26,12 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
duration: const Duration(milliseconds: 350),
);
_slideAnimation = Tween(begin: const Offset(0, -0.1), end: Offset.zero)
.animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
_slideAnimation = Tween(
begin: const Offset(0, -0.1),
end: Offset.zero,
).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
);
_controller.forward();
}
@@ -40,13 +43,93 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
}
// ------------------------------------------------------------------
// HEADER SUMMARY CARD
// STATUS BADGE (WHITE + GRADIENT BORDER)
// ------------------------------------------------------------------
Widget statusBadge(String status, double scale) {
final s = status.toLowerCase();
LinearGradient gradient;
Color textColor;
switch (s) {
case 'paid':
gradient = const LinearGradient(
colors: [Color(0xFF2ECC71), Color(0xFF27AE60)],
);
textColor = const Color(0xFF27AE60);
break;
case 'pending':
gradient = const LinearGradient(
colors: [
Color(0xFF5B8DEF), // Soft Blue
Color(0xFF7B5CFA), // Purple Blue
],
);
textColor = const Color(0xFF5B8DEF);
break;
case 'overdue':
gradient = const LinearGradient(
colors: [
Color(0xFFFFB300), // Amber
Color(0xFFFF6F00), // Deep Orange
],
);
textColor = const Color(0xFFFF6F00);
break;
case 'cancelled':
case 'failed':
gradient = const LinearGradient(
colors: [Color(0xFFE74C3C), Color(0xFFC0392B)],
);
textColor = const Color(0xFFE74C3C);
break;
default:
gradient = const LinearGradient(
colors: [Color(0xFF95A5A6), Color(0xFF7F8C8D)],
);
textColor = Colors.grey.shade700;
}
return Container(
padding: const EdgeInsets.all(1.5),
decoration: BoxDecoration(
gradient: gradient,
borderRadius: BorderRadius.circular(20 * scale),
),
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 14 * scale,
vertical: 6 * scale,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18 * scale),
),
child: Text(
status.toUpperCase(),
style: TextStyle(
fontSize: 12 * scale,
fontWeight: FontWeight.w700,
color: textColor,
letterSpacing: .6,
),
),
),
);
}
// ------------------------------------------------------------------
// HEADER CARD
// ------------------------------------------------------------------
Widget headerCard(Map invoice, double scale) {
return Container(
width: double.infinity,
padding: EdgeInsets.all(16 * scale), // tighter
margin: EdgeInsets.only(bottom: 14 * scale), // closer
padding: EdgeInsets.all(16 * scale),
margin: EdgeInsets.only(bottom: 14 * scale),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade400, Colors.indigo.shade600],
@@ -76,40 +159,34 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
"Date: ${invoice['invoice_date']}",
style: TextStyle(color: Colors.white70, fontSize: 13 * scale),
),
SizedBox(height: 8 * scale),
Container(
padding: EdgeInsets.symmetric(
vertical: 5 * scale, horizontal: 12 * scale),
decoration: BoxDecoration(
color: Colors.white.withOpacity(.2),
borderRadius: BorderRadius.circular(40 * scale),
),
child: Text(
invoice['status'] ?? "Unknown",
style: TextStyle(
color: Colors.white,
fontSize: 13 * scale,
),
),
)
SizedBox(height: 10 * scale),
/// STATUS BADGE
statusBadge(invoice['status'] ?? 'Unknown', scale),
],
),
);
}
// ------------------------------------------------------------------
// SECTION HEADER (Closer Spacing)
// SECTION HEADER
// ------------------------------------------------------------------
Widget sectionHeader(
String title, IconData icon, bool expanded, Function() tap, double scale) {
String title,
IconData icon,
bool expanded,
VoidCallback tap,
double scale,
) {
return GestureDetector(
onTap: tap,
child: Container(
padding: EdgeInsets.all(12 * scale), // tighter
margin: EdgeInsets.only(bottom: 8 * scale), // closer
padding: EdgeInsets.all(12 * scale),
margin: EdgeInsets.only(bottom: 8 * scale),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.indigo.shade500, Colors.blue.shade400]),
colors: [Colors.indigo.shade500, Colors.blue.shade400],
),
borderRadius: BorderRadius.circular(12 * scale),
boxShadow: [
BoxShadow(
@@ -128,14 +205,18 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
style: TextStyle(
fontSize: 15 * scale,
color: Colors.white,
fontWeight: FontWeight.bold),
fontWeight: FontWeight.bold,
),
),
const Spacer(),
AnimatedRotation(
turns: expanded ? .5 : 0,
duration: const Duration(milliseconds: 250),
child: Icon(Icons.keyboard_arrow_down,
color: Colors.white, size: 20 * scale),
child: Icon(
Icons.keyboard_arrow_down,
color: Colors.white,
size: 20 * scale,
),
)
],
),
@@ -144,7 +225,7 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
}
// ------------------------------------------------------------------
// SECTION BODY (Closer Spacing)
// SECTION BODY
// ------------------------------------------------------------------
Widget sectionBody(bool visible, List<Widget> children, double scale) {
return AnimatedCrossFade(
@@ -155,9 +236,8 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
secondChild: SlideTransition(
position: _slideAnimation,
child: Container(
width: double.infinity,
padding: EdgeInsets.all(14 * scale), // tighter
margin: EdgeInsets.only(bottom: 10 * scale), // closer
padding: EdgeInsets.all(14 * scale),
margin: EdgeInsets.only(bottom: 10 * scale),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12 * scale),
@@ -165,7 +245,8 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
BoxShadow(
blurRadius: 7 * scale,
offset: Offset(0, 2 * scale),
color: Colors.black.withOpacity(.07))
color: Colors.black.withOpacity(.07),
)
],
),
child: Column(children: children),
@@ -175,11 +256,11 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
}
// ------------------------------------------------------------------
// DETAIL ROW (Closer Spacing)
// DETAIL ROW
// ------------------------------------------------------------------
Widget detailRow(IconData icon, String label, dynamic value, double scale) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 5 * scale), // closer
padding: EdgeInsets.symmetric(vertical: 5 * scale),
child: Row(
children: [
Icon(icon, size: 18 * scale, color: Colors.blueGrey),
@@ -196,11 +277,18 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
),
Expanded(
flex: 4,
child: Text(
child: label == "Status"
? Align(
alignment: Alignment.centerRight,
child: statusBadge(value ?? 'Unknown', scale),
)
: Text(
value?.toString() ?? "N/A",
textAlign: TextAlign.right,
style:
TextStyle(fontSize: 14 * scale, fontWeight: FontWeight.bold),
style: TextStyle(
fontSize: 14 * scale,
fontWeight: FontWeight.bold,
),
),
)
],
@@ -208,60 +296,6 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
);
}
// ------------------------------------------------------------------
// TIMELINE (Closer Spacing)
// ------------------------------------------------------------------
Widget invoiceTimeline(double scale) {
final steps = ["Invoice Created", "Payment Received", "Out for Delivery", "Completed"];
final icons = [Icons.receipt_long, Icons.payments, Icons.local_shipping, Icons.verified];
return Container(
padding: EdgeInsets.all(16 * scale),
margin: EdgeInsets.only(bottom: 16 * scale), // closer
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(14 * scale),
boxShadow: [
BoxShadow(
blurRadius: 7 * scale,
color: Colors.black.withOpacity(.1),
offset: Offset(0, 3 * scale),
)
],
),
child: Column(
children: List.generate(steps.length, (i) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4 * scale), // closer
child: Row(
children: [
Column(
children: [
Icon(icons[i], size: 22 * scale, color: Colors.indigo),
if (i < steps.length - 1)
Container(
height: 28 * scale,
width: 2 * scale,
color: Colors.indigo.shade300,
)
],
),
SizedBox(width: 10 * scale),
Expanded(
child: Text(
steps[i],
style:
TextStyle(fontSize: 14 * scale, fontWeight: FontWeight.w600),
),
)
],
),
);
}),
),
);
}
// ------------------------------------------------------------------
// MAIN BUILD
// ------------------------------------------------------------------
@@ -272,21 +306,30 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
final scale = (width / 390).clamp(0.85, 1.25);
return ListView(
padding: EdgeInsets.all(14 * scale), // slightly tighter
padding: EdgeInsets.all(14 * scale),
children: [
headerCard(invoice, scale),
invoiceTimeline(scale),
sectionHeader("Invoice Summary", Icons.receipt_long, s1,
() => setState(() => s1 = !s1), scale),
sectionHeader(
"Invoice Summary",
Icons.receipt_long,
s1,
() => setState(() => s1 = !s1),
scale,
),
sectionBody(s1, [
detailRow(Icons.numbers, "Invoice No", invoice['invoice_number'], scale),
detailRow(Icons.calendar_today, "Date", invoice['invoice_date'], scale),
detailRow(Icons.label, "Status", invoice['status'], scale),
], scale),
sectionHeader("Customer Details", Icons.person, s2,
() => setState(() => s2 = !s2), scale),
sectionHeader(
"Customer Details",
Icons.person,
s2,
() => setState(() => s2 = !s2),
scale,
),
sectionBody(s2, [
detailRow(Icons.person_outline, "Name", invoice['customer_name'], scale),
detailRow(Icons.mail, "Email", invoice['customer_email'], scale),
@@ -294,22 +337,19 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
detailRow(Icons.location_on, "Address", invoice['customer_address'], scale),
], scale),
sectionHeader("Amount Details", Icons.currency_rupee, s3,
() => setState(() => s3 = !s3), scale),
sectionHeader(
"Amount Details",
Icons.currency_rupee,
s3,
() => setState(() => s3 = !s3),
scale,
),
sectionBody(s3, [
detailRow(Icons.money, "Amount", invoice['final_amount'], scale),
detailRow(Icons.percent, "GST percent %", invoice['gst_percent'], scale),
detailRow(Icons.percent, "GST amount %", invoice['gst_amount'], scale),
detailRow(Icons.percent, "GST %", invoice['gst_percent'], scale),
detailRow(Icons.percent, "GST Amount", invoice['gst_amount'], scale),
detailRow(Icons.summarize, "Total", invoice['final_amount_with_gst'], scale),
], scale),
// sectionHeader("Payment Details", Icons.payment, s4,
// () => setState(() => s4 = !s4), scale),
// sectionBody(s4, [
// detailRow(Icons.credit_card, "Method", invoice['payment_method'], scale),
// detailRow(Icons.confirmation_number, "Reference",
// invoice['reference_no'], scale),
// ], scale),
],
);
}

View File

@@ -388,26 +388,26 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "10.0.9"
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.9"
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.2"
lints:
dependency: transitive
description:
@@ -436,10 +436,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.16.0"
version: "1.17.0"
mime:
dependency: transitive
description:
@@ -777,10 +777,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev"
source: hosted
version: "0.7.4"
version: "0.7.7"
typed_data:
dependency: transitive
description:
@@ -865,10 +865,10 @@ packages:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.2.0"
video_player:
dependency: "direct main"
description: