Files
kent_logistics_app/lib/screens/order_invoice_screen.dart
2025-12-18 12:45:26 +05:30

417 lines
13 KiB
Dart

import 'package:flutter/material.dart';
import '../services/dio_client.dart';
import '../services/order_service.dart';
class OrderInvoiceScreen extends StatefulWidget {
final String orderId;
const OrderInvoiceScreen({super.key, required this.orderId});
@override
State<OrderInvoiceScreen> createState() => _OrderInvoiceScreenState();
}
class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
with SingleTickerProviderStateMixin {
bool loading = true;
bool controllerInitialized = false;
Map invoice = {};
late AnimationController _controller;
late Animation<Offset> _slideAnimation;
bool s1 = true;
bool s2 = false;
bool s3 = false;
bool s4 = false;
@override
void initState() {
super.initState();
initializeController();
load();
}
void initializeController() {
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 280),
);
_slideAnimation = Tween<Offset>(
begin: const Offset(0, -0.05),
end: Offset.zero,
).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
);
controllerInitialized = true;
_controller.forward();
}
@override
void dispose() {
if (controllerInitialized) _controller.dispose();
super.dispose();
}
Future<void> load() async {
final service = OrderService(DioClient.getInstance(context));
final res = await service.getInvoice(widget.orderId);
if (res["success"] == true) {
invoice = res["invoice"] ?? {};
}
if (mounted) setState(() => loading = false);
}
@override
Widget build(BuildContext context) {
final items = invoice["items"] as List? ?? [];
final width = MediaQuery.of(context).size.width;
final scale = (width / 430).clamp(0.85, 1.18);
return Scaffold(
appBar: AppBar(
title: const Text("Invoice"),
),
body: loading || !controllerInitialized
? const Center(child: CircularProgressIndicator())
: ListView(
padding: EdgeInsets.all(16 * scale),
children: [
_headerCard(scale),
// SUMMARY SECTION
_sectionHeader("Invoice Summary", Icons.receipt, s1, () {
setState(() => s1 = !s1);
}, scale),
_sectionBody(s1, [
_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),
_sectionBody(s2, [
_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.summarize, "Final With GST",
invoice['final_amount_with_gst'], scale),
], scale),
// CUSTOMER SECTION
_sectionHeader("Customer Details", Icons.person, s3, () {
setState(() => s3 = !s3);
}, scale),
_sectionBody(s3, [
_detailRow(Icons.person, "Name", invoice['customer_name'], scale),
_detailRow(Icons.business, "Company", invoice['company_name'], scale),
_detailRow(Icons.mail, "Email", invoice['customer_email'], scale),
_detailRow(Icons.phone, "Mobile", invoice['customer_mobile'], scale),
_detailRow(Icons.location_on, "Address", invoice['customer_address'], scale),
], scale),
// ITEMS SECTION
_sectionHeader("Invoice Items", Icons.shopping_cart, s4, () {
setState(() => s4 = !s4);
}, scale),
_sectionBody(
s4,
items.isEmpty
? [Text("No items found", style: TextStyle(fontSize: 14 * scale))]
: items.map((item) => _itemTile(item, scale)).toList(),
scale,
),
],
),
);
}
// ---------------- HEADER CARD ----------------
Widget _headerCard(double scale) {
return Container(
padding: EdgeInsets.all(18 * scale),
margin: EdgeInsets.only(bottom: 18 * scale),
decoration: BoxDecoration(
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))
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Invoice #${invoice['invoice_number'] ?? '-'}",
style: TextStyle(
fontSize: 22 * scale,
fontWeight: FontWeight.bold,
color: Colors.white)),
SizedBox(height: 6 * 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),
borderRadius: BorderRadius.circular(50 * scale),
),
child: Text(
invoice["status"]?.toString() ?? "Unknown",
style: TextStyle(color: Colors.white, fontSize: 14 * scale),
),
)
],
),
);
}
// ---------------- SECTION HEADER ----------------
Widget _sectionHeader(
String title, IconData icon, bool expanded, Function toggle, double scale) {
return GestureDetector(
onTap: () => toggle(),
child: Container(
padding: EdgeInsets.all(14 * scale),
margin: EdgeInsets.only(bottom: 10 * scale),
decoration: BoxDecoration(
gradient:
LinearGradient(colors: [Colors.blue.shade400, Colors.indigo.shade500]),
borderRadius: BorderRadius.circular(14 * scale),
),
child: Row(
children: [
Icon(icon, color: Colors.white, size: 20 * scale),
SizedBox(width: 10 * scale),
Text(title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15 * scale,
color: Colors.white)),
const Spacer(),
AnimatedRotation(
turns: expanded ? .5 : 0,
duration: const Duration(milliseconds: 250),
child: Icon(Icons.keyboard_arrow_down,
color: Colors.white, size: 22 * scale),
)
],
),
),
);
}
// ---------------- SECTION BODY ----------------
Widget _sectionBody(bool visible, List<Widget> children, double scale) {
if (!controllerInitialized) return const SizedBox();
return AnimatedCrossFade(
duration: const Duration(milliseconds: 250),
firstChild: const SizedBox.shrink(),
secondChild: SlideTransition(
position: _slideAnimation,
child: Container(
padding: EdgeInsets.all(16 * scale),
margin: EdgeInsets.only(bottom: 14 * scale),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14 * scale),
color: Colors.white,
boxShadow: [
BoxShadow(
blurRadius: 8 * scale,
offset: Offset(0, 3 * scale),
color: Colors.black.withOpacity(.08)),
],
),
child: Column(children: children),
),
),
crossFadeState:
visible ? CrossFadeState.showSecond : CrossFadeState.showFirst,
);
}
// ---------------- DETAIL ROW ----------------
Widget _detailRow(IconData icon, String label, dynamic value, double scale) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 6 * scale),
child: Row(
children: [
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)),
),
Expanded(
child: Text(
value?.toString() ?? "N/A",
textAlign: TextAlign.end,
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 15 * scale),
),
)
],
),
);
}
// ---------------- ITEM TILE ----------------
Widget _itemTile(Map item, double scale) {
final qty = item['qty'] ?? 0;
final price = item['price'] ?? 0;
final total = item['ttl_amount'] ?? 0;
return Container(
padding: EdgeInsets.all(16 * scale),
margin: EdgeInsets.only(bottom: 14 * scale),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16 * scale),
color: Colors.white,
boxShadow: [
BoxShadow(
blurRadius: 8 * scale,
offset: Offset(0, 3 * scale),
color: Colors.black.withOpacity(.08)),
],
border: Border.all(color: Colors.grey.shade300, width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// TITLE
Row(
children: [
Container(
padding: EdgeInsets.all(8 * scale),
decoration: BoxDecoration(
color: Colors.indigo.shade50,
borderRadius: BorderRadius.circular(10 * scale),
),
child: Icon(Icons.inventory_2,
color: Colors.indigo, size: 20 * scale),
),
SizedBox(width: 12 * scale),
Expanded(
child: Text(
item['description'] ?? "Item",
style: TextStyle(
fontSize: 16 * scale, fontWeight: FontWeight.w600),
),
),
],
),
SizedBox(height: 14 * scale),
// QTY & PRICE
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_itemBadge(Icons.numbers, "Qty", qty.toString(), false, scale),
_itemBadge(Icons.currency_rupee, "Price", "$price", false, scale),
],
),
SizedBox(height: 12 * scale),
// TOTAL
Container(
padding: EdgeInsets.symmetric(
vertical: 10 * scale, horizontal: 14 * scale),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12 * scale),
color: Colors.indigo.shade50,
border: Border.all(color: Colors.indigo, width: 1.5 * scale),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(Icons.summarize,
size: 18 * scale, color: Colors.indigo),
SizedBox(width: 6 * scale),
Text(
"Total:",
style: TextStyle(
fontSize: 15 * scale,
fontWeight: FontWeight.w600,
color: Colors.indigo,
),
),
],
),
Text(
"$total",
style: TextStyle(
fontSize: 17 * scale,
fontWeight: FontWeight.bold,
color: Colors.indigo,
),
),
],
),
)
],
),
);
}
// ---------------- BADGE ----------------
Widget _itemBadge(
IconData icon, String label, String value, bool highlight, double scale) {
return Container(
padding: EdgeInsets.symmetric(vertical: 8 * scale, horizontal: 12 * scale),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12 * scale),
color: highlight ? Colors.indigo.shade50 : Colors.grey.shade100,
border: Border.all(
color: highlight ? Colors.indigo : Colors.grey.shade300,
width: highlight ? 1.5 * scale : 1),
),
child: Row(
children: [
Icon(icon,
size: 16 * scale,
color: highlight ? Colors.indigo : Colors.grey),
SizedBox(width: 4 * scale),
Text(
"$label: ",
style: TextStyle(
fontSize: 13 * scale,
fontWeight: FontWeight.w600,
color: highlight ? Colors.indigo : Colors.grey.shade700,
),
),
Text(
value,
style: TextStyle(
fontSize: 14 * scale,
fontWeight: FontWeight.bold,
color: highlight ? Colors.indigo : Colors.black,
),
)
],
),
);
}
}