Your changes
This commit is contained in:
File diff suppressed because one or more lines are too long
3
.gitignore
vendored
3
.gitignore
vendored
@@ -32,3 +32,6 @@ windows/flutter/ephemeral/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# FVM Version Cache
|
||||
.fvm/
|
||||
BIN
assets/Images/K.png
Normal file
BIN
assets/Images/K.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -1,3 +1,3 @@
|
||||
class ApiConfig {
|
||||
static const String baseUrl = "http://10.207.50.74:8000/api";
|
||||
static const String baseUrl = "http://192.168.0.100:8000/api";
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ class KentApp extends StatelessWidget {
|
||||
useMaterial3: true,
|
||||
textTheme: GoogleFonts.interTextTheme(),
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.red),
|
||||
scaffoldBackgroundColor: const Color(0xfff8f6ff), // your light background
|
||||
scaffoldBackgroundColor: const Color(0xFFE8F0FF), // your light background
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Colors.indigo, // FIX
|
||||
foregroundColor: Colors.white, // white text + icons
|
||||
|
||||
@@ -4,7 +4,11 @@ import '../services/invoice_service.dart';
|
||||
|
||||
class InvoiceInstallmentScreen extends StatefulWidget {
|
||||
final int invoiceId;
|
||||
const InvoiceInstallmentScreen({super.key, required this.invoiceId});
|
||||
|
||||
const InvoiceInstallmentScreen({
|
||||
super.key,
|
||||
required this.invoiceId,
|
||||
});
|
||||
|
||||
@override
|
||||
State<InvoiceInstallmentScreen> createState() =>
|
||||
@@ -35,39 +39,188 @@ class _InvoiceInstallmentScreenState extends State<InvoiceInstallmentScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Installments")),
|
||||
backgroundColor: Colors.grey.shade100,
|
||||
appBar: AppBar(
|
||||
title: const Text("Installments"),
|
||||
elevation: 1,
|
||||
),
|
||||
body: loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: installments.isEmpty
|
||||
? const Center(
|
||||
child: Text("Installments not created yet",
|
||||
style: TextStyle(fontSize: 18)),
|
||||
)
|
||||
? _buildEmptyState()
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: width * 0.04,
|
||||
vertical: 16,
|
||||
),
|
||||
itemCount: installments.length,
|
||||
itemBuilder: (_, i) {
|
||||
final inst = installments[i];
|
||||
return Card(
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
"Amount: ₹${inst['amount']?.toString() ?? '0'}"),
|
||||
subtitle: Column(
|
||||
return InstallmentCard(inst: installments[i]);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.receipt_long,
|
||||
size: 70, color: Colors.grey.shade400),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
"No Installments Created",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class InstallmentCard extends StatelessWidget {
|
||||
final Map inst;
|
||||
|
||||
const InstallmentCard({super.key, required this.inst});
|
||||
|
||||
String getString(key) => inst[key]?.toString() ?? "N/A";
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final isTablet = width > 600;
|
||||
|
||||
final padding = isTablet ? 28.0 : 20.0;
|
||||
final amountSize = isTablet ? 30.0 : 26.0;
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 18),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(padding),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Amount + Payment Method Row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Date: ${inst['installment_date'] ?? 'N/A'}"),
|
||||
Text(
|
||||
"Payment: ${inst['payment_method'] ?? 'N/A'}"),
|
||||
Text(
|
||||
"Reference: ${inst['reference_no'] ?? 'N/A'}"),
|
||||
"₹${getString('amount')}",
|
||||
style: TextStyle(
|
||||
fontSize: amountSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
),
|
||||
|
||||
// Payment Chip
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: isTablet ? 8 : 6,
|
||||
horizontal: isTablet ? 16 : 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
child: Text(
|
||||
getString('payment_method'),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.blue.shade700,
|
||||
fontSize: isTablet ? 15 : 13.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: isTablet ? 24 : 18),
|
||||
|
||||
// Responsive Info Rows
|
||||
buildInfoRow(
|
||||
Icons.calendar_month,
|
||||
"Date",
|
||||
getString("installment_date"),
|
||||
isTablet
|
||||
),
|
||||
SizedBox(height: isTablet ? 14 : 10),
|
||||
buildInfoRow(
|
||||
Icons.confirmation_number,
|
||||
"Reference",
|
||||
getString("reference_no"),
|
||||
isTablet
|
||||
),
|
||||
|
||||
SizedBox(height: isTablet ? 24 : 18),
|
||||
Divider(color: Colors.grey.shade300, thickness: 1),
|
||||
SizedBox(height: isTablet ? 10 : 6),
|
||||
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
"Installment #${inst['id'] ?? ''}",
|
||||
style: TextStyle(
|
||||
fontSize: isTablet ? 15 : 13,
|
||||
color: Colors.grey.shade600,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Responsive info row builder
|
||||
Widget buildInfoRow(IconData icon, String label, String value, bool isTablet) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: isTablet ? 24 : 20, color: Colors.grey.shade600),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
"$label:",
|
||||
style: TextStyle(
|
||||
fontSize: isTablet ? 17 : 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: isTablet ? 16 : 15,
|
||||
color: Colors.grey.shade800,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,250 +12,572 @@ class DashboardScreen extends StatefulWidget {
|
||||
State<DashboardScreen> createState() => _DashboardScreenState();
|
||||
}
|
||||
|
||||
class _DashboardScreenState extends State<DashboardScreen> {
|
||||
class _DashboardScreenState extends State<DashboardScreen>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _scaleCtrl;
|
||||
late AnimationController _shineCtrl;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_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();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||
|
||||
// STEP 1: Try refresh token BEFORE any API calls
|
||||
await auth.tryRefreshToken(context);
|
||||
|
||||
// STEP 2: Now safe to load dashboard
|
||||
final dash = Provider.of<DashboardProvider>(context, listen: false);
|
||||
dash.init(context);
|
||||
await dash.loadSummary(context);
|
||||
|
||||
// STEP 3: Load marks AFTER refresh
|
||||
final marks = Provider.of<MarkListProvider>(context, listen: false);
|
||||
marks.init(context);
|
||||
await marks.loadMarks(context);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scaleCtrl.dispose();
|
||||
_shineCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _showAddMarkForm() {
|
||||
// ============================================================
|
||||
// CENTERED ADD MARK POPUP
|
||||
// ============================================================
|
||||
void _showAddMarkForm(double scale) {
|
||||
final markCtrl = TextEditingController();
|
||||
final originCtrl = TextEditingController();
|
||||
final destCtrl = TextEditingController();
|
||||
|
||||
showModalBottomSheet(
|
||||
showDialog(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
|
||||
barrierDismissible: true,
|
||||
builder: (_) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
MediaQuery.of(context).viewInsets.bottom + 20,
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
Text(
|
||||
"Add Mark No",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
style: TextStyle(
|
||||
fontSize: 20 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.indigo,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextField(controller: markCtrl, decoration: const InputDecoration(labelText: "Mark No")),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(height: 18 * scale),
|
||||
|
||||
TextField(controller: originCtrl, decoration: const InputDecoration(labelText: "Origin")),
|
||||
const SizedBox(height: 12),
|
||||
_inputField(markCtrl, "Mark No", scale),
|
||||
SizedBox(height: 12 * scale),
|
||||
_inputField(originCtrl, "Origin", scale),
|
||||
SizedBox(height: 12 * scale),
|
||||
_inputField(destCtrl, "Destination", scale),
|
||||
|
||||
TextField(controller: destCtrl, decoration: const InputDecoration(labelText: "Destination")),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(height: 22 * scale),
|
||||
|
||||
ElevatedButton(
|
||||
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 required")));
|
||||
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);
|
||||
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 {
|
||||
final msg = res['message'] ?? "Failed";
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(res['message'] ?? "Failed")),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text("Submit"),
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// MAIN UI (Responsive)
|
||||
// ============================================================
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final auth = Provider.of<AuthProvider>(context);
|
||||
final dash = Provider.of<DashboardProvider>(context);
|
||||
final marks = Provider.of<MarkListProvider>(context);
|
||||
|
||||
final name = auth.user?['customer_name'] ?? 'User';
|
||||
|
||||
if (dash.loading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final name = auth.user?['customer_name'] ?? "User";
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final scale = (width / 430).clamp(0.88, 1.08);
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(18),
|
||||
padding: EdgeInsets.all(16 * scale),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// HEADER
|
||||
Text(
|
||||
"Welcome, $name 👋",
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ORDER SUMMARY
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// WELCOME CARD WITH ANIMATION
|
||||
AnimatedBuilder(
|
||||
animation: _scaleCtrl,
|
||||
builder: (_, __) {
|
||||
return Transform.scale(
|
||||
scale: _scaleCtrl.value,
|
||||
child: Stack(
|
||||
children: [
|
||||
_statBox("Active", dash.activeOrders, Colors.blue),
|
||||
_statBox("In Transit", dash.inTransitOrders, Colors.orange),
|
||||
_statBox("Delivered", dash.deliveredOrders, Colors.green),
|
||||
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),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_valueCard("Total Value", dash.totalValue),
|
||||
const SizedBox(height: 10),
|
||||
_valueCard("Raw Amount", "₹${dash.totalRaw.toStringAsFixed(2)}"),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 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),
|
||||
|
||||
// ADD + VIEW ALL BUTTONS SIDE BY SIDE
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text("Add Mark No"),
|
||||
onPressed: _showAddMarkForm,
|
||||
Text(
|
||||
"Latest Mark Numbers",
|
||||
style: TextStyle(
|
||||
fontSize: 18 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
|
||||
if (marks.marks.length > 0)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
),
|
||||
if (marks.marks.isNotEmpty)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const MarkListScreen()),
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const MarkListScreen()),
|
||||
);
|
||||
},
|
||||
child: const Text(
|
||||
child: Text(
|
||||
"View All →",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
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: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_summaryTile("Active Orders", dash.activeOrders ?? 0,
|
||||
Colors.blue, Icons.inventory, scale),
|
||||
_summaryTile("In Transit", dash.inTransitOrders ?? 0,
|
||||
Colors.orange, Icons.local_shipping, scale),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: 12 * scale),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_summaryTile("Delivered", dash.deliveredOrders ?? 0,
|
||||
Colors.green, Icons.check_circle, scale),
|
||||
_summaryTile("Total Value", "₹${dash.totalValue ?? 0}",
|
||||
Colors.teal, Icons.money, scale),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: 16 * scale),
|
||||
|
||||
_rawAmountTile("Raw Amount", rawAmount, scale),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 2 * scale),
|
||||
|
||||
SizedBox(
|
||||
width: 100 * scale,
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 13 * scale,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// MARK LIST (only 10 latest)
|
||||
const Text(
|
||||
"Latest Mark Numbers",
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
|
||||
if (marks.loading)
|
||||
const Center(child: CircularProgressIndicator())
|
||||
else
|
||||
Column(
|
||||
children: List.generate(
|
||||
marks.marks.length > 10 ? 10 : marks.marks.length,
|
||||
(i) {
|
||||
final m = marks.marks[i];
|
||||
return Card(
|
||||
child: ListTile(
|
||||
title: Text(m['mark_no']),
|
||||
subtitle: Text("${m['origin']} → ${m['destination']}"),
|
||||
trailing: Text(
|
||||
m['status'],
|
||||
style: const TextStyle(color: Colors.indigo),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
Icon(icon, size: 28 * scale, color: color),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// UI WIDGETS
|
||||
Widget _statBox(String title, int value, Color color) {
|
||||
Widget _rawAmountTile(String title, String value, double scale) {
|
||||
return Container(
|
||||
width: 110,
|
||||
padding: const EdgeInsets.all(14),
|
||||
padding: EdgeInsets.all(14 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.deepPurple.shade50,
|
||||
borderRadius: BorderRadius.circular(14 * scale),
|
||||
),
|
||||
child: Column(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(value.toString(),
|
||||
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
Text(title, style: const TextStyle(fontSize: 14)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _valueCard(String title, String value) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.indigo.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14, fontWeight: FontWeight.w600, color: Colors.grey)),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
style: TextStyle(
|
||||
fontSize: 20 * scale,
|
||||
color: Colors.deepPurple,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.indigo,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 13 * scale,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Icon(Icons.currency_rupee,
|
||||
size: 28 * scale, color: Colors.deepPurple),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// MARK TILE
|
||||
// ============================================================
|
||||
Widget _markTile(dynamic m, double scale) {
|
||||
return Container(
|
||||
margin: EdgeInsets.only(bottom: 12 * scale),
|
||||
padding: EdgeInsets.all(14 * scale),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF42A5F5), Color(0xFF80DEEA)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(14 * scale),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final profile = Provider.of<UserProfileProvider>(
|
||||
context, listen: false).profile;
|
||||
final profile =
|
||||
Provider.of<UserProfileProvider>(context, listen: false).profile;
|
||||
|
||||
nameCtrl.text = profile?.customerName ?? '';
|
||||
companyCtrl.text = profile?.companyName ?? '';
|
||||
@@ -32,8 +32,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
final provider =
|
||||
Provider.of<UserProfileProvider>(context, listen: false);
|
||||
final provider = Provider.of<UserProfileProvider>(context, listen: false);
|
||||
|
||||
final data = {
|
||||
"customer_name": nameCtrl.text,
|
||||
@@ -44,14 +43,16 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||
"pincode": pincodeCtrl.text,
|
||||
};
|
||||
|
||||
final success =
|
||||
await provider.sendProfileUpdateRequest(context, data);
|
||||
final success = await provider.sendProfileUpdateRequest(context, data);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(success
|
||||
content: Text(
|
||||
success
|
||||
? "Request submitted. Wait for admin approval."
|
||||
: "Failed to submit request")),
|
||||
: "Failed to submit request",
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (success) Navigator.pop(context);
|
||||
@@ -59,38 +60,179 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Edit Profile")),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(18),
|
||||
final darkBlue = const Color(0xFF003B73);
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Color(0xFFB3E5FC), Color(0xFFE1F5FE)],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: SafeArea(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
double scale = (screenWidth / 390).clamp(0.75, 1.3);
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: EdgeInsets.symmetric(horizontal: 18 * scale),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(height: 8 * scale),
|
||||
|
||||
/// BACK BUTTON
|
||||
IconButton(
|
||||
icon: Icon(Icons.arrow_back_ios_new, size: 22 * scale),
|
||||
color: Colors.red,
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
|
||||
SizedBox(height: 10 * scale),
|
||||
|
||||
/// TITLE
|
||||
Center(
|
||||
child: Text(
|
||||
"Edit Profile",
|
||||
style: TextStyle(
|
||||
color: darkBlue,
|
||||
fontSize: 26 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 20 * scale),
|
||||
|
||||
/// RESPONSIVE CENTERED FORM CARD
|
||||
Center(
|
||||
child: Container(
|
||||
width: screenWidth > 650 ? 550 : double.infinity,
|
||||
padding: EdgeInsets.all(24 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.95),
|
||||
borderRadius: BorderRadius.circular(20 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12.withOpacity(0.15),
|
||||
blurRadius: 18 * scale,
|
||||
offset: Offset(0, 6 * scale),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_field("Name", nameCtrl),
|
||||
_field("Company", companyCtrl),
|
||||
_field("Email", emailCtrl),
|
||||
_field("Mobile", mobileCtrl),
|
||||
_field("Address", addressCtrl),
|
||||
_field("Pincode", pincodeCtrl),
|
||||
_buildField("Full Name", nameCtrl, scale),
|
||||
_buildField("Company Name", companyCtrl, scale),
|
||||
_buildField("Email Address", emailCtrl, scale),
|
||||
_buildField("Mobile Number", mobileCtrl, scale),
|
||||
_buildField("Address", addressCtrl, scale),
|
||||
_buildField("Pincode", pincodeCtrl, scale),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
SizedBox(height: 25 * scale),
|
||||
|
||||
/// RESPONSIVE GRADIENT SUBMIT BUTTON
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 50 * scale,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(14 * scale),
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
Color(0xFF0052D4),
|
||||
Color(0xFF4364F7),
|
||||
Color(0xFF6FB1FC),
|
||||
],
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.blueAccent.withOpacity(0.4),
|
||||
blurRadius: 10 * scale,
|
||||
offset: Offset(0, 4 * scale),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: _submit,
|
||||
child: const Text("Submit Update Request"),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(14 * scale),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"Submit Update Request",
|
||||
style: TextStyle(
|
||||
fontSize: 17 * scale,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 30 * scale),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _field(String title, TextEditingController ctrl) {
|
||||
/// Reusable Responsive TextField Builder
|
||||
Widget _buildField(
|
||||
String label, TextEditingController ctrl, double scale) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 14),
|
||||
padding: EdgeInsets.only(bottom: 18 * scale),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12.withOpacity(0.06),
|
||||
blurRadius: 10 * scale,
|
||||
offset: Offset(0, 3 * scale),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
controller: ctrl,
|
||||
style: TextStyle(fontSize: 15 * scale),
|
||||
decoration: InputDecoration(
|
||||
labelText: title,
|
||||
border: OutlineInputBorder(),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
labelText: label,
|
||||
labelStyle: TextStyle(
|
||||
color: const Color(0xFF003B73),
|
||||
fontSize: 14 * scale,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16 * scale),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16 * scale),
|
||||
borderSide: BorderSide(
|
||||
color: const Color(0xFF003B73),
|
||||
width: 2 * scale,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:pdf/widgets.dart' as pw;
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
import '../services/dio_client.dart';
|
||||
import '../services/invoice_service.dart';
|
||||
import '../widgets/invoice_detail_view.dart';
|
||||
|
||||
class InvoiceDetailScreen extends StatefulWidget {
|
||||
final int invoiceId;
|
||||
@@ -20,149 +26,127 @@ class _InvoiceDetailScreenState extends State<InvoiceDetailScreen> {
|
||||
load();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// ⭐ LOAD INVOICE FROM API
|
||||
// -------------------------------------------------------
|
||||
Future<void> load() async {
|
||||
final service = InvoiceService(DioClient.getInstance(context));
|
||||
try {
|
||||
final res = await service.getInvoiceDetails(widget.invoiceId);
|
||||
|
||||
if (res['success'] == true) {
|
||||
invoice = res['invoice'] ?? {};
|
||||
} else {
|
||||
invoice = {};
|
||||
}
|
||||
} catch (e) {
|
||||
invoice = {};
|
||||
} finally {
|
||||
if (mounted) setState(() => loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
loading = false;
|
||||
setState(() {});
|
||||
}
|
||||
// -------------------------------------------------------
|
||||
// ⭐ GENERATE + SAVE PDF TO DOWNLOADS FOLDER
|
||||
// (No Permission Needed)
|
||||
// -------------------------------------------------------
|
||||
Future<File> generatePDF() async {
|
||||
final pdf = pw.Document();
|
||||
|
||||
/// ---------- REUSABLE ROW ----------
|
||||
Widget row(String label, dynamic value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
pdf.addPage(
|
||||
pw.Page(
|
||||
build: (context) => pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: const TextStyle(fontSize: 14, color: Colors.grey)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value?.toString().isNotEmpty == true ? value.toString() : "N/A",
|
||||
textAlign: TextAlign.end,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
pw.Text(
|
||||
"INVOICE DETAILS",
|
||||
style: pw.TextStyle(fontSize: 26, fontWeight: pw.FontWeight.bold),
|
||||
),
|
||||
pw.SizedBox(height: 20),
|
||||
|
||||
pw.Text("Invoice ID: ${invoice['id'] ?? '-'}"),
|
||||
pw.Text("Amount: ₹${invoice['amount'] ?? '-'}"),
|
||||
pw.Text("Status: ${invoice['status'] ?? '-'}"),
|
||||
pw.Text("Date: ${invoice['date'] ?? '-'}"),
|
||||
pw.Text("Customer: ${invoice['customer_name'] ?? '-'}"),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// ⭐ SAFEST WAY (Android 10–14)
|
||||
final downloadsDir = await getDownloadsDirectory();
|
||||
|
||||
final filePath = "${downloadsDir!.path}/invoice_${invoice['id']}.pdf";
|
||||
final file = File(filePath);
|
||||
|
||||
await file.writeAsBytes(await pdf.save());
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("PDF saved to Downloads:\n$filePath")),
|
||||
);
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// ⭐ SHARE THE SAVED PDF FILE
|
||||
// -------------------------------------------------------
|
||||
Future<void> sharePDF() async {
|
||||
final file = await generatePDF();
|
||||
|
||||
await Share.shareXFiles(
|
||||
[XFile(file.path)],
|
||||
text: "Invoice Details",
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final scale = (width / 430).clamp(0.88, 1.08);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Invoice Details")),
|
||||
body: loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
|
||||
/// ================ INVOICE DATA ================
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ListView(
|
||||
children: [
|
||||
/// -------- Invoice Summary --------
|
||||
const Text(
|
||||
"Invoice Summary",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
"Invoice Details",
|
||||
style: TextStyle(
|
||||
fontSize: 18 * scale,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
row("Invoice No", invoice['invoice_number']),
|
||||
row("Invoice Date", invoice['invoice_date']),
|
||||
row("Due Date", invoice['due_date']),
|
||||
row("Status", invoice['status']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
/// -------- Customer Details --------
|
||||
const Text(
|
||||
"Customer Details",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
row("Name", invoice['customer_name']),
|
||||
row("Company", invoice['company_name']),
|
||||
row("Email", invoice['customer_email']),
|
||||
row("Mobile", invoice['customer_mobile']),
|
||||
row("Address", invoice['customer_address']),
|
||||
row("Pincode", invoice['pincode']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
/// -------- Amounts & Taxes --------
|
||||
const Text(
|
||||
"Amounts",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
row("Final Amount", invoice['final_amount']),
|
||||
row("Tax Type", invoice['tax_type']),
|
||||
row("GST %", invoice['gst_percent']),
|
||||
row("GST Amount", invoice['gst_amount']),
|
||||
row("Final with GST", invoice['final_amount_with_gst']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
/// -------- Payment Details --------
|
||||
const Text(
|
||||
"Payment Details",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
row("Payment Method", invoice['payment_method']),
|
||||
row("Reference No", invoice['reference_no']),
|
||||
row("Notes", invoice['notes']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
/// -------- PDF --------
|
||||
if (invoice['pdf_path'] != null)
|
||||
ElevatedButton.icon(
|
||||
// ⭐ PDF + SHARE BUTTONS
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.picture_as_pdf),
|
||||
label: const Text("Download PDF"),
|
||||
onPressed: () {},
|
||||
style:
|
||||
ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||
onPressed: generatePDF,
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
/// -------- Invoice Items --------
|
||||
if (invoice['items'] != null)
|
||||
const Text(
|
||||
"Invoice Items",
|
||||
style:
|
||||
TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.share),
|
||||
onPressed: sharePDF,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
...List.generate(invoice['items']?.length ?? 0, (i) {
|
||||
final item = invoice['items'][i];
|
||||
return Card(
|
||||
child: ListTile(
|
||||
title: Text(item['description'] ?? "Item"),
|
||||
subtitle: Text("Qty: ${item['qty'] ?? 0}"),
|
||||
trailing: Text(
|
||||
"₹${item['ttl_amount'] ?? 0}",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.indigo),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
|
||||
body: loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: invoice.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
"No Invoice Data Found",
|
||||
style: TextStyle(
|
||||
fontSize: 18 * scale,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding: EdgeInsets.all(12 * scale),
|
||||
child: InvoiceDetailView(invoice: invoice),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/invoice_installment_screen.dart';
|
||||
import '../providers/invoice_provider.dart';
|
||||
import '../services/dio_client.dart';
|
||||
import '../services/invoice_service.dart';
|
||||
import 'invoice_detail_screen.dart';
|
||||
|
||||
|
||||
class InvoiceScreen extends StatefulWidget {
|
||||
const InvoiceScreen({super.key});
|
||||
|
||||
@@ -15,10 +12,11 @@ class InvoiceScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _InvoiceScreenState extends State<InvoiceScreen> {
|
||||
String searchQuery = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
Provider.of<InvoiceProvider>(context, listen: false)
|
||||
.loadInvoices(context);
|
||||
@@ -29,78 +27,263 @@ class _InvoiceScreenState extends State<InvoiceScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final provider = Provider.of<InvoiceProvider>(context);
|
||||
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final scale = (width / 430).clamp(0.88, 1.08);
|
||||
|
||||
if (provider.loading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (provider.invoices.isEmpty) {
|
||||
return const Center(
|
||||
child: Text("No invoices found", style: TextStyle(fontSize: 18)));
|
||||
}
|
||||
// 🔍 Filter invoices based on search query
|
||||
final filteredInvoices = provider.invoices.where((inv) {
|
||||
final q = searchQuery.toLowerCase();
|
||||
return inv['invoice_number'].toString().toLowerCase().contains(q) ||
|
||||
inv['invoice_date'].toString().toLowerCase().contains(q) ||
|
||||
inv['formatted_amount'].toString().toLowerCase().contains(q) ||
|
||||
inv['status'].toString().toLowerCase().contains(q);
|
||||
}).toList();
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: provider.invoices.length,
|
||||
return Column(
|
||||
children: [
|
||||
// 🔍 SEARCH BAR
|
||||
Container(
|
||||
margin: EdgeInsets.fromLTRB(16 * scale, 16 * scale, 16 * scale, 8 * scale),
|
||||
padding: EdgeInsets.symmetric(horizontal: 14 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12.withOpacity(0.08),
|
||||
blurRadius: 8 * scale,
|
||||
offset: Offset(0, 3 * scale),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
onChanged: (v) => setState(() => searchQuery = v),
|
||||
style: TextStyle(fontSize: 14 * scale),
|
||||
decoration: InputDecoration(
|
||||
icon: Icon(Icons.search, size: 22 * scale),
|
||||
hintText: "Search Invoice Number, Date, Amount...",
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 📄 LIST OF INVOICES
|
||||
Expanded(
|
||||
child: filteredInvoices.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
"No invoices found",
|
||||
style: TextStyle(
|
||||
fontSize: 18 * scale,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: EdgeInsets.all(16 * scale),
|
||||
itemCount: filteredInvoices.length,
|
||||
itemBuilder: (_, i) {
|
||||
final inv = provider.invoices[i];
|
||||
final inv = filteredInvoices[i];
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
color: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16 * scale),
|
||||
),
|
||||
elevation: 3,
|
||||
margin: EdgeInsets.only(bottom: 14 * scale),
|
||||
child: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.all(16 * scale),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
/// Invoice Number
|
||||
Text(
|
||||
"Invoice ${inv['invoice_number'] ?? 'N/A'}",
|
||||
style: const TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.bold),
|
||||
style: TextStyle(
|
||||
fontSize: 20 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 6),
|
||||
Text("Date: ${inv['invoice_date'] ?? 'N/A'}"),
|
||||
Text("Status: ${inv['status'] ?? 'N/A'}"),
|
||||
Text("Amount: ₹${inv['formatted_amount'] ?? '0'}"),
|
||||
SizedBox(height: 8 * scale),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
/// Date + Amount
|
||||
Text(
|
||||
"Date: ${inv['invoice_date'] ?? 'N/A'}",
|
||||
style: TextStyle(fontSize: 15 * scale),
|
||||
),
|
||||
Text(
|
||||
"Amount: ₹${inv['formatted_amount'] ?? '0'}",
|
||||
style: TextStyle(fontSize: 15 * scale),
|
||||
),
|
||||
|
||||
SizedBox(height: 16 * scale),
|
||||
|
||||
/// BUTTONS ROW
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
child: const Text("Invoice Details"),
|
||||
onPressed: () {
|
||||
Expanded(
|
||||
child: GradientButton(
|
||||
text: "Invoice Details",
|
||||
fontSize: 15 * scale,
|
||||
radius: 12 * scale,
|
||||
padding: 14 * scale,
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
Color(0xFF1976D2),
|
||||
Color(0xFF42A5F5),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => InvoiceDetailScreen(
|
||||
invoiceId: inv['invoice_id'],
|
||||
),
|
||||
builder: (_) =>
|
||||
InvoiceDetailScreen(
|
||||
invoiceId:
|
||||
inv['invoice_id']),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
OutlinedButton(
|
||||
child: const Text("Installments"),
|
||||
onPressed: () {
|
||||
SizedBox(width: 12 * scale),
|
||||
|
||||
Expanded(
|
||||
child: GradientButton(
|
||||
text: "Installments",
|
||||
fontSize: 15 * scale,
|
||||
radius: 12 * scale,
|
||||
padding: 14 * scale,
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
Color(0xFF43A047),
|
||||
Color(0xFF81C784),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => InvoiceInstallmentScreen(
|
||||
invoiceId: inv['invoice_id'],
|
||||
),
|
||||
builder: (_) =>
|
||||
InvoiceInstallmentScreen(
|
||||
invoiceId:
|
||||
inv['invoice_id']),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
/// STATUS BADGE
|
||||
Positioned(
|
||||
right: 12 * scale,
|
||||
top: 12 * scale,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 10 * scale,
|
||||
vertical: 6 * scale,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatusColor(inv['status']),
|
||||
borderRadius: BorderRadius.circular(12 * scale),
|
||||
),
|
||||
child: Text(
|
||||
inv['status'] ?? 'N/A',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Status Color Helper
|
||||
Color _getStatusColor(String? status) {
|
||||
switch (status?.toLowerCase()) {
|
||||
case 'pending':
|
||||
return Colors.orange;
|
||||
case 'in transit':
|
||||
return Colors.blue;
|
||||
case 'overdue':
|
||||
return Colors.redAccent;
|
||||
case 'paid':
|
||||
return Colors.green;
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
/// -------------------------------------------------------
|
||||
/// RESPONSIVE GRADIENT BUTTON
|
||||
/// -------------------------------------------------------
|
||||
class GradientButton extends StatelessWidget {
|
||||
final String text;
|
||||
final Gradient gradient;
|
||||
final VoidCallback onTap;
|
||||
|
||||
final double fontSize;
|
||||
final double padding;
|
||||
final double radius;
|
||||
|
||||
const GradientButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.gradient,
|
||||
required this.onTap,
|
||||
required this.fontSize,
|
||||
required this.padding,
|
||||
required this.radius,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
onTap: onTap,
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
gradient: gradient,
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(vertical: padding),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,27 +26,27 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
Future<void> _login() async {
|
||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||
|
||||
final loginId = cLoginId.text.trim();
|
||||
final password = cPassword.text.trim();
|
||||
final id = cLoginId.text.trim();
|
||||
final pass = cPassword.text.trim();
|
||||
|
||||
if (loginId.isEmpty || password.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Please enter login id and password')),
|
||||
);
|
||||
if (id.isEmpty || pass.isEmpty) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(content: Text("Please fill all fields")));
|
||||
return;
|
||||
}
|
||||
|
||||
final res = await auth.login(context, loginId, password);
|
||||
final res = await auth.login(context, id, pass);
|
||||
|
||||
if (res['success'] == true) {
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pushReplacement(
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const MainBottomNav()),
|
||||
);
|
||||
} else {
|
||||
final msg = res['message']?.toString() ?? 'Login failed';
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(res['message'] ?? "Login failed")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,38 +55,113 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
final auth = Provider.of<AuthProvider>(context);
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
|
||||
/// ⭐ RESPONSIVE SCALE
|
||||
final scale = (width / 390).clamp(0.85, 1.25);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
backgroundColor: const Color(0xFFE8F0FF),
|
||||
|
||||
body: Stack(
|
||||
children: [
|
||||
/// 🔵 Floating Back Button (Responsive Position + Size)
|
||||
Positioned(
|
||||
top: 40 * scale,
|
||||
left: 12 * scale,
|
||||
child: Material(
|
||||
elevation: 6 * scale,
|
||||
color: Colors.indigo.shade700,
|
||||
shape: const CircleBorder(),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const WelcomeScreen()),
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(10 * scale),
|
||||
child: Icon(Icons.arrow_back,
|
||||
color: Colors.white, size: 20 * scale),
|
||||
),
|
||||
),
|
||||
),
|
||||
title: const Text('Login'),
|
||||
),
|
||||
|
||||
body: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: width * 0.06, vertical: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
RoundedInput(
|
||||
controller: cLoginId,
|
||||
hint: 'Email / Mobile / Customer ID',
|
||||
/// 📦 Center White Card (Responsive)
|
||||
Center(
|
||||
child: Container(
|
||||
width: width * 0.87,
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 28 * scale,
|
||||
horizontal: 20 * scale,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
RoundedInput(
|
||||
controller: cPassword,
|
||||
hint: 'Password',
|
||||
obscure: true,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(22 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
blurRadius: 18 * scale,
|
||||
spreadRadius: 1,
|
||||
offset: Offset(0, 6 * scale),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
PrimaryButton(label: 'Login', onTap: _login, busy: auth.loading),
|
||||
],
|
||||
),
|
||||
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"Login",
|
||||
style: TextStyle(
|
||||
fontSize: 26 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.indigo.shade700,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 25 * scale),
|
||||
|
||||
/// Login ID Input
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFD8E7FF),
|
||||
borderRadius: BorderRadius.circular(14 * scale),
|
||||
),
|
||||
child: RoundedInput(
|
||||
controller: cLoginId,
|
||||
hint: "Email / Mobile / Customer ID",
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 16 * scale),
|
||||
|
||||
/// Password Input
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFD8E7FF),
|
||||
borderRadius: BorderRadius.circular(14 * scale),
|
||||
),
|
||||
child: RoundedInput(
|
||||
controller: cPassword,
|
||||
hint: "Password",
|
||||
obscure: true,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 25 * scale),
|
||||
|
||||
/// Login Button
|
||||
PrimaryButton(
|
||||
label: "Login",
|
||||
onTap: _login,
|
||||
busy: auth.loading,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,9 +17,7 @@ class MainBottomNavState extends State<MainBottomNav> {
|
||||
int _currentIndex = 0;
|
||||
|
||||
void setIndex(int index) {
|
||||
setState(() {
|
||||
_currentIndex = index;
|
||||
});
|
||||
setState(() => _currentIndex = index);
|
||||
}
|
||||
|
||||
final List<Widget> _screens = const [
|
||||
@@ -30,27 +28,167 @@ class MainBottomNavState extends State<MainBottomNav> {
|
||||
SettingsScreen(),
|
||||
];
|
||||
|
||||
final List<IconData> _icons = const [
|
||||
Icons.dashboard_outlined,
|
||||
Icons.shopping_bag_outlined,
|
||||
Icons.receipt_long_outlined,
|
||||
Icons.chat_bubble_outline,
|
||||
Icons.settings_outlined,
|
||||
];
|
||||
|
||||
final List<String> _labels = const [
|
||||
"Dashboard",
|
||||
"Orders",
|
||||
"Invoice",
|
||||
"Chat",
|
||||
"Settings",
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final scale = (width / 390).clamp(0.85, 1.20);
|
||||
|
||||
final containerPadding = 8 * scale;
|
||||
|
||||
return Scaffold(
|
||||
appBar: const MainAppBar(),
|
||||
body: _screens[_currentIndex],
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
currentIndex: _currentIndex,
|
||||
selectedItemColor: Colors.red,
|
||||
unselectedItemColor: Colors.black,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
onTap: (index) {
|
||||
setState(() => _currentIndex = index);
|
||||
},
|
||||
items: const [
|
||||
BottomNavigationBarItem(icon: Icon(Icons.dashboard_outlined), label: "Dashboard"),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.shopping_bag_outlined), label: "Orders"),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.receipt_long_outlined), label: "Invoice"),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.chat_bubble_outline), label: "Chat"),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.settings_outlined), label: "Settings"),
|
||||
|
||||
bottomNavigationBar: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 10 * scale,
|
||||
right: 10 * scale,
|
||||
bottom: 10 * scale,
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final totalWidth = constraints.maxWidth;
|
||||
|
||||
// inner width (after padding)
|
||||
final contentWidth = totalWidth - (containerPadding * 2);
|
||||
|
||||
final safeContentWidth =
|
||||
contentWidth > 0 ? contentWidth : totalWidth;
|
||||
|
||||
final itemWidth = safeContentWidth / _icons.length;
|
||||
|
||||
final indicatorWidth = 70 * scale;
|
||||
final indicatorHeight = 70 * scale;
|
||||
|
||||
double left = (_currentIndex * itemWidth) +
|
||||
(itemWidth / 2) -
|
||||
(indicatorWidth / 2);
|
||||
|
||||
/// ⭐ FIX: explicitly convert clamp to double
|
||||
final double safeLeft = left
|
||||
.clamp(0, safeContentWidth - indicatorWidth)
|
||||
.toDouble();
|
||||
|
||||
return Container(
|
||||
height: 100 * scale,
|
||||
padding: EdgeInsets.symmetric(horizontal: containerPadding),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(28 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 20 * scale,
|
||||
offset: Offset(0, 8 * scale),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
/// ⭐ Indicator - safe positioned
|
||||
AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
curve: Curves.easeOut,
|
||||
top: 10 * scale,
|
||||
left: safeLeft,
|
||||
child: Container(
|
||||
width: indicatorWidth,
|
||||
height: indicatorHeight,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
Color(0xFF4F46E5),
|
||||
Color(0xFF06B6D4),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20 * scale),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
_icons[_currentIndex],
|
||||
size: 22 * scale,
|
||||
color: Colors.white,
|
||||
),
|
||||
SizedBox(height: 2 * scale),
|
||||
Text(
|
||||
_labels[_currentIndex],
|
||||
style: TextStyle(
|
||||
fontSize: 10 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
/// ⭐ Icon Row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: List.generate(_icons.length, (index) {
|
||||
final selected = index == _currentIndex;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => setIndex(index),
|
||||
child: SizedBox(
|
||||
width: itemWidth,
|
||||
height: 100 * scale,
|
||||
child: Center(
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
opacity: selected ? 0 : 1,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
_icons[index],
|
||||
size: 22 * scale,
|
||||
color: Colors.black87,
|
||||
),
|
||||
SizedBox(height: 4 * scale),
|
||||
Text(
|
||||
_labels[index],
|
||||
style: TextStyle(
|
||||
fontSize: 10 * scale,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,15 +10,13 @@ class MarkListScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MarkListScreenState extends State<MarkListScreen> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final provider = Provider.of<MarkListProvider>(context, listen: false);
|
||||
provider.init(context);
|
||||
provider.loadMarks(context); // Load full list again
|
||||
provider.loadMarks(context);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,27 +24,112 @@ 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(
|
||||
title: const Text("All Mark Numbers"),
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
iconTheme: const IconThemeData(color: Colors.black),
|
||||
|
||||
title: Text(
|
||||
"All Mark Numbers",
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 20 * scale,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
body: marks.loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: EdgeInsets.all(10 * scale), // smaller padding
|
||||
itemCount: marks.marks.length,
|
||||
itemBuilder: (_, i) {
|
||||
final m = marks.marks[i];
|
||||
|
||||
return Card(
|
||||
child: ListTile(
|
||||
title: Text(m['mark_no']),
|
||||
subtitle: Text("${m['origin']} → ${m['destination']}"),
|
||||
trailing: Text(
|
||||
m['status'],
|
||||
style: const TextStyle(color: Colors.indigo),
|
||||
return Container(
|
||||
margin: EdgeInsets.only(bottom: 10 * scale), // reduced margin
|
||||
padding: EdgeInsets.all(12 * scale), // smaller padding
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
Color(0xFF2196F3),
|
||||
Color(0xFF64B5F6),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(14 * scale), // smaller radius
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.06),
|
||||
blurRadius: 5 * scale, // smaller shadow
|
||||
offset: Offset(0, 2 * scale),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// LEFT TEXT
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// MARK NUMBER
|
||||
Text(
|
||||
m['mark_no'],
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16 * scale, // reduced font
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 3 * scale),
|
||||
|
||||
// ROUTE
|
||||
Text(
|
||||
"${m['origin']} → ${m['destination']}",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13 * scale, // reduced font
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// STATUS BADGE
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 10 * scale,
|
||||
vertical: 5 * scale, // smaller badge
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.92),
|
||||
borderRadius: BorderRadius.circular(24 * scale),
|
||||
),
|
||||
child: Text(
|
||||
m['status'],
|
||||
style: TextStyle(
|
||||
fontSize: 11.5 * scale,
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/dio_client.dart';
|
||||
import '../services/order_service.dart';
|
||||
|
||||
|
||||
class OrderDetailScreen extends StatefulWidget {
|
||||
final String orderId;
|
||||
const OrderDetailScreen({super.key, required this.orderId});
|
||||
@@ -14,6 +14,7 @@ class OrderDetailScreen extends StatefulWidget {
|
||||
class _OrderDetailScreenState extends State<OrderDetailScreen> {
|
||||
bool loading = true;
|
||||
Map order = {};
|
||||
final Map<String, bool> _expanded = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -33,105 +34,298 @@ class _OrderDetailScreenState extends State<OrderDetailScreen> {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Widget _row(String label, dynamic value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label,
|
||||
style: const TextStyle(fontSize: 14, color: Colors.grey)),
|
||||
Text(value?.toString() ?? 'N/A',
|
||||
style: const TextStyle(
|
||||
fontSize: 15, fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
);
|
||||
String _initials(String? s) {
|
||||
if (s == null || s.isEmpty) return "I";
|
||||
final parts = s.split(" ");
|
||||
return parts.take(2).map((e) => e[0].toUpperCase()).join();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = order['items'] ?? [];
|
||||
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final scale = (width / 430).clamp(0.85, 1.20);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Order Details")),
|
||||
backgroundColor: const Color(0xFFF0F6FF),
|
||||
appBar: AppBar(
|
||||
title: const Text("Order Details"),
|
||||
elevation: 0,
|
||||
),
|
||||
body: loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ListView(
|
||||
children: [
|
||||
// ---------------- ORDER SUMMARY ----------------
|
||||
const Text(
|
||||
"Order Summary",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
_row("Order ID", order['order_id']),
|
||||
_row("Mark No", order['mark_no']),
|
||||
_row("Origin", order['origin']),
|
||||
_row("Destination", order['destination']),
|
||||
_row("Status", order['status']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
const Text(
|
||||
"Totals",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
_row("CTN", order['ctn']),
|
||||
_row("Qty", order['qty']),
|
||||
_row("Total Qty", order['ttl_qty']),
|
||||
_row("Amount", "₹${order['ttl_amount'] ?? 0}"),
|
||||
_row("CBM", order['cbm']),
|
||||
_row("Total CBM", order['ttl_cbm']),
|
||||
_row("KG", order['kg']),
|
||||
_row("Total KG", order['ttl_kg']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
// ---------------- ORDER ITEMS ----------------
|
||||
const Text(
|
||||
"Order Items",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
...List.generate(items.length, (i) {
|
||||
final item = items[i];
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: EdgeInsets.all(16 * scale),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item['description'] ?? "No description",
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
|
||||
_row("Qty", item['qty']),
|
||||
_row("Unit", item['unit']),
|
||||
_row("CBM", item['cbm']),
|
||||
_row("KG", item['kg']),
|
||||
_row("Amount", "₹${item['ttl_amount'] ?? 0}"),
|
||||
_row("Shop No", item['shop_no']),
|
||||
_summaryCard(scale),
|
||||
SizedBox(height: 18 * scale),
|
||||
_itemsSection(items, scale),
|
||||
SizedBox(height: 18 * scale),
|
||||
_totalsSection(scale),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// SUMMARY CARD
|
||||
// -----------------------------
|
||||
Widget _summaryCard(double scale) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(18 * scale),
|
||||
decoration: _cardDecoration(scale),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Order Summary",
|
||||
style: TextStyle(fontSize: 20 * scale, fontWeight: FontWeight.bold)),
|
||||
SizedBox(height: 12 * scale),
|
||||
_infoRow("Order ID", order['order_id'], scale),
|
||||
_infoRow("Mark No", order['mark_no'], scale),
|
||||
_infoRow("Origin", order['origin'], scale),
|
||||
_infoRow("Destination", order['destination'], scale),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _infoRow(String title, dynamic value, double scale) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 6 * scale),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(title,
|
||||
style: TextStyle(color: Colors.grey, fontSize: 14 * scale)),
|
||||
Text(value?.toString() ?? "-",
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15 * scale)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// ORDER ITEMS SECTION
|
||||
// -----------------------------
|
||||
Widget _itemsSection(List items, double scale) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(18 * scale),
|
||||
decoration: _cardDecoration(scale),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Order Items",
|
||||
style: TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
|
||||
SizedBox(height: 16 * scale),
|
||||
...List.generate(items.length, (i) {
|
||||
return _expandableItem(items[i], i, scale);
|
||||
})
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// EXPANDABLE ITEM
|
||||
// -----------------------------
|
||||
Widget _expandableItem(Map item, int index, double scale) {
|
||||
final id = "item_$index";
|
||||
_expanded[id] = _expanded[id] ?? false;
|
||||
|
||||
final description = item['description'] ?? "Item";
|
||||
final initials = _initials(description);
|
||||
final imageUrl = item['image'] ?? "";
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
margin: EdgeInsets.only(bottom: 16 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14 * scale),
|
||||
border: Border.all(color: Colors.black12),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
minVerticalPadding: 10 * scale,
|
||||
leading: _avatar(imageUrl, initials, scale),
|
||||
title: Text(description,
|
||||
style:
|
||||
TextStyle(fontWeight: FontWeight.bold, fontSize: 15 * scale)),
|
||||
trailing: Transform.rotate(
|
||||
angle: (_expanded[id]! ? 3.14 : 0),
|
||||
child: Icon(Icons.keyboard_arrow_down, size: 24 * scale),
|
||||
),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_expanded[id] = !_expanded[id]!;
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
if (_expanded[id]!)
|
||||
Padding(
|
||||
padding: EdgeInsets.all(12 * scale),
|
||||
child: Column(
|
||||
children: [
|
||||
_pill("Qty", Icons.list_alt, "${item['qty']}",
|
||||
Colors.blue.shade100, scale),
|
||||
_pill("Unit", Icons.category, "${item['unit']}",
|
||||
Colors.orange.shade100, scale),
|
||||
_pill("KG", Icons.scale, "${item['kg']}",
|
||||
Colors.red.shade100, scale),
|
||||
_pill("CBM", Icons.straighten, "${item['cbm']}",
|
||||
Colors.purple.shade100, scale),
|
||||
_pill("Shop", Icons.storefront, "${item['shop_no']}",
|
||||
Colors.grey.shade300, scale),
|
||||
_pill("Amount", Icons.currency_rupee,
|
||||
"₹${item['ttl_amount']}",
|
||||
Colors.green.shade100, scale),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// AVATAR (RESPONSIVE)
|
||||
// -----------------------------
|
||||
Widget _avatar(String url, String initials, double scale) {
|
||||
return Container(
|
||||
width: 48 * scale,
|
||||
height: 48 * scale,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade200,
|
||||
borderRadius: BorderRadius.circular(10 * scale),
|
||||
),
|
||||
child: url.isNotEmpty
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10 * scale),
|
||||
child: Image.network(url, fit: BoxFit.cover,
|
||||
errorBuilder: (c, e, s) => Center(
|
||||
child: Text(initials,
|
||||
style: TextStyle(
|
||||
fontSize: 18 * scale,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold)),
|
||||
)),
|
||||
)
|
||||
: Center(
|
||||
child: Text(initials,
|
||||
style: TextStyle(
|
||||
fontSize: 18 * scale,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// COLOR-CODED PILL (RESPONSIVE)
|
||||
// -----------------------------
|
||||
Widget _pill(
|
||||
String title, IconData icon, String value, Color bgColor, double scale) {
|
||||
return Container(
|
||||
margin: EdgeInsets.only(bottom: 12 * scale),
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 14 * scale, vertical: 12 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(12 * scale),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 20 * scale, color: Colors.black54),
|
||||
SizedBox(width: 10 * scale),
|
||||
Text("$title: ",
|
||||
style:
|
||||
TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * scale)),
|
||||
Expanded(
|
||||
child: Text(value,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700, fontSize: 15 * scale)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// TOTAL SECTION
|
||||
// -----------------------------
|
||||
Widget _totalsSection(double scale) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(18 * scale),
|
||||
decoration: _cardDecoration(scale),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Totals",
|
||||
style: TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
|
||||
SizedBox(height: 12 * scale),
|
||||
_totalRow("Total Qty", order['ttl_qty'], scale),
|
||||
_totalRow("Total KG", order['ttl_kg'], scale),
|
||||
SizedBox(height: 12 * scale),
|
||||
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(vertical: 20 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.shade50,
|
||||
borderRadius: BorderRadius.circular(12 * scale),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text("₹${order['ttl_amount'] ?? 0}",
|
||||
style: TextStyle(
|
||||
color: Colors.green,
|
||||
fontSize: 28 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
)),
|
||||
SizedBox(height: 4 * scale),
|
||||
Text("Total Amount",
|
||||
style: TextStyle(
|
||||
color: Colors.black54, fontSize: 14 * scale)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _totalRow(String title, dynamic value, double scale) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(title,
|
||||
style: TextStyle(color: Colors.grey, fontSize: 14 * scale)),
|
||||
Text(value?.toString() ?? "0",
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15 * scale)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// CARD DECORATION
|
||||
// -----------------------------
|
||||
BoxDecoration _cardDecoration(double scale) {
|
||||
return BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
blurRadius: 8 * scale,
|
||||
offset: Offset(0, 3 * scale)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ 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});
|
||||
@@ -11,139 +10,406 @@ class OrderInvoiceScreen extends StatefulWidget {
|
||||
State<OrderInvoiceScreen> createState() => _OrderInvoiceScreenState();
|
||||
}
|
||||
|
||||
class _OrderInvoiceScreenState extends State<OrderInvoiceScreen> {
|
||||
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 (res["success"] == true) {
|
||||
invoice = res["invoice"] ?? {};
|
||||
}
|
||||
|
||||
loading = false;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Widget _row(String label, dynamic value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label,
|
||||
style: const TextStyle(fontSize: 14, color: Colors.grey)),
|
||||
Text(value?.toString() ?? "N/A",
|
||||
style:
|
||||
const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (mounted) setState(() => loading = false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = invoice['items'] ?? [];
|
||||
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
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ListView(
|
||||
children: [
|
||||
const Text("Invoice Summary",
|
||||
style:
|
||||
TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
_row("Invoice No", invoice['invoice_number']),
|
||||
_row("Invoice Date", invoice['invoice_date']),
|
||||
_row("Due Date", invoice['due_date']),
|
||||
_row("Payment Method", invoice['payment_method']),
|
||||
_row("Reference No", invoice['reference_no']),
|
||||
_row("Status", invoice['status']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
const Text("Amount Details",
|
||||
style:
|
||||
TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
_row("Amount (without GST)", invoice['final_amount']),
|
||||
_row("GST Amount", invoice['gst_amount']),
|
||||
_row("Final Amount With GST",
|
||||
invoice['final_amount_with_gst']),
|
||||
_row("Tax Type", invoice['tax_type']),
|
||||
_row("CGST %", invoice['cgst_percent']),
|
||||
_row("SGST %", invoice['sgst_percent']),
|
||||
_row("IGST %", invoice['igst_percent']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
_row("Customer Name", invoice['customer_name']),
|
||||
_row("Company Name", invoice['company_name']),
|
||||
_row("Email", invoice['customer_email']),
|
||||
_row("Mobile", invoice['customer_mobile']),
|
||||
_row("Address", invoice['customer_address']),
|
||||
_row("Pincode", invoice['pincode']),
|
||||
_row("Notes", invoice['notes']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
// PDF DOWNLOAD
|
||||
if (invoice['pdf_path'] != null)
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
// open pdf
|
||||
},
|
||||
icon: const Icon(Icons.picture_as_pdf, color: Colors.red),
|
||||
label: const Text("Download PDF"),
|
||||
appBar: AppBar(
|
||||
title: const Text("Invoice"),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
const Text("Invoice Items",
|
||||
style:
|
||||
TextStyle(fontSize: 17, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
...List.generate(items.length, (i) {
|
||||
final item = items[i];
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: ListTile(
|
||||
title: Text(item['description'] ?? "Item"),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
body: loading || !controllerInitialized
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: ListView(
|
||||
padding: EdgeInsets.all(16 * scale),
|
||||
children: [
|
||||
Text("Qty: ${item['qty'] ?? 0}"),
|
||||
Text("Price: ₹${item['price'] ?? 0}"),
|
||||
_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 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: Text(
|
||||
"₹${item['ttl_amount'] ?? 0}",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.indigo),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------- 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,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/order_provider.dart';
|
||||
import 'order_detail_screen.dart';
|
||||
import 'order_shipment_screen.dart';
|
||||
import 'order_invoice_screen.dart';
|
||||
import 'order_track_screen.dart';
|
||||
|
||||
@@ -14,6 +13,7 @@ class OrdersScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _OrdersScreenState extends State<OrdersScreen> {
|
||||
String searchQuery = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -31,77 +31,290 @@ class _OrdersScreenState extends State<OrdersScreen> {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: provider.orders.length,
|
||||
itemBuilder: (_, i) {
|
||||
final o = provider.orders[i];
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final scale = (screenWidth / 420).clamp(0.85, 1.15);
|
||||
|
||||
// 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);
|
||||
}).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
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.all(16 * scale),
|
||||
itemCount: filteredOrders.length,
|
||||
itemBuilder: (context, i) {
|
||||
final order = filteredOrders[i];
|
||||
return _orderCard(order, scale);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ORDER CARD UI
|
||||
Widget _orderCard(Map<String, dynamic> o, double scale) {
|
||||
final progress = getProgress(o['status']);
|
||||
final badgeColor = getStatusColor(o['status']);
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
elevation: 3 * scale,
|
||||
color: Colors.white,
|
||||
margin: EdgeInsets.only(bottom: 16 * scale),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16 * scale),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: EdgeInsets.all(16 * scale),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Order ID: ${o['order_id']}",
|
||||
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
Text(o['description']),
|
||||
Text("₹ ${o['amount']}"),
|
||||
Text(o['status'], style: const TextStyle(color: Colors.indigo)),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// TOP ROW
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_btn("Order", () => _openOrderDetails(o['order_id'])),
|
||||
_btn("Shipment", () => _openShipment(o['order_id'])),
|
||||
_btn("Invoice", () => _openInvoice(o['order_id'])),
|
||||
_btn("Track", () => _openTrack(o['order_id'])),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: 10 * scale),
|
||||
|
||||
Text(
|
||||
o['description'],
|
||||
style: TextStyle(fontSize: 14 * scale),
|
||||
),
|
||||
|
||||
SizedBox(height: 5 * scale),
|
||||
|
||||
Text(
|
||||
"₹${o['amount']}",
|
||||
style: TextStyle(
|
||||
fontSize: 16 * scale,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 18 * scale),
|
||||
|
||||
_AnimatedProgressBar(progress: progress, scale: scale),
|
||||
|
||||
SizedBox(height: 18 * scale),
|
||||
|
||||
// BUTTONS
|
||||
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),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _btn(String text, VoidCallback onTap) {
|
||||
// BUTTON UI
|
||||
Widget _btn(IconData icon, String text, Color fg, Color bg,
|
||||
VoidCallback onTap, double scale) {
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(12 * scale),
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
color: Colors.indigo.shade50,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 20 * scale,
|
||||
vertical: 12 * scale,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
borderRadius: BorderRadius.circular(12 * scale),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 18 * scale, color: fg),
|
||||
SizedBox(width: 8 * scale),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: fg,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14 * scale,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Text(text, style: const TextStyle(color: Colors.indigo)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// NAVIGATION
|
||||
void _openOrderDetails(String id) {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (_) => OrderDetailScreen(orderId: id)));
|
||||
}
|
||||
|
||||
void _openShipment(String id) {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (_) => OrderShipmentScreen(orderId: id)));
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => OrderDetailScreen(orderId: id)),
|
||||
);
|
||||
}
|
||||
|
||||
void _openInvoice(String id) {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (_) => OrderInvoiceScreen(orderId: id)));
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => OrderInvoiceScreen(orderId: id)),
|
||||
);
|
||||
}
|
||||
|
||||
void _openTrack(String id) {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (_) => OrderTrackScreen(orderId: id)));
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => OrderTrackScreen(orderId: id)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// PROGRESS BAR
|
||||
class _AnimatedProgressBar extends StatelessWidget {
|
||||
final double progress;
|
||||
final double scale;
|
||||
|
||||
const _AnimatedProgressBar({required this.progress, required this.scale});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxW = constraints.maxWidth;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: 10 * scale,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(20 * scale),
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 650),
|
||||
curve: Curves.easeInOut,
|
||||
height: 10 * scale,
|
||||
width: maxW * progress,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20 * scale),
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
Color(0xFF4F8CFF),
|
||||
Color(0xFF8A4DFF),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// PROGRESS VALUES
|
||||
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;
|
||||
|
||||
return 0.05;
|
||||
}
|
||||
|
||||
// STATUS COLORS
|
||||
Color getStatusColor(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;
|
||||
|
||||
return Colors.black54;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// lib/screens/order_track_screen.dart
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/dio_client.dart';
|
||||
import '../services/order_service.dart';
|
||||
|
||||
|
||||
class OrderTrackScreen extends StatefulWidget {
|
||||
final String orderId;
|
||||
const OrderTrackScreen({super.key, required this.orderId});
|
||||
@@ -11,54 +12,579 @@ class OrderTrackScreen extends StatefulWidget {
|
||||
State<OrderTrackScreen> createState() => _OrderTrackScreenState();
|
||||
}
|
||||
|
||||
class _OrderTrackScreenState extends State<OrderTrackScreen> {
|
||||
class _OrderTrackScreenState extends State<OrderTrackScreen>
|
||||
with TickerProviderStateMixin {
|
||||
bool loading = true;
|
||||
Map data = {};
|
||||
|
||||
Map<String, dynamic>? shipment;
|
||||
Map<String, dynamic> trackData = {};
|
||||
|
||||
late final AnimationController progressController;
|
||||
late final AnimationController shipController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
load();
|
||||
}
|
||||
|
||||
Future<void> load() async {
|
||||
final service = OrderService(DioClient.getInstance(context));
|
||||
final res = await service.trackOrder(widget.orderId);
|
||||
progressController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 900),
|
||||
);
|
||||
|
||||
if (res['success'] == true) {
|
||||
data = res['track'];
|
||||
}
|
||||
shipController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1600),
|
||||
)..repeat(reverse: true);
|
||||
|
||||
loading = false;
|
||||
setState(() {});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _loadData());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
progressController.dispose();
|
||||
shipController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// ---------------- LOAD DATA ----------------
|
||||
Future<void> _loadData() async {
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() => loading = true);
|
||||
|
||||
try {
|
||||
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}),
|
||||
]);
|
||||
|
||||
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"] ?? {})
|
||||
: {};
|
||||
} catch (_) {}
|
||||
|
||||
final target = _computeProgress();
|
||||
|
||||
if (mounted) {
|
||||
try {
|
||||
await progressController.animateTo(
|
||||
target,
|
||||
curve: Curves.easeInOutCubic,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (mounted) setState(() => loading = false);
|
||||
}
|
||||
|
||||
// ---------------- PROGRESS LOGIC ----------------
|
||||
double _computeProgress() {
|
||||
final status = (trackData["shipment_status"] ?? "")
|
||||
.toString()
|
||||
.toLowerCase();
|
||||
|
||||
if (status.contains("delivered")) return 1.0;
|
||||
if (status.contains("dispatched")) return 0.85;
|
||||
if (status.contains("transit")) return 0.65;
|
||||
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 {
|
||||
final d = DateTime.parse(v.toString()).toLocal();
|
||||
return "${d.day}/${d.month}/${d.year}";
|
||||
} catch (_) {
|
||||
return v.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- UI BUILD ----------------
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final scale = (width / 430).clamp(0.75, 1.25);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Track Order")),
|
||||
appBar: AppBar(
|
||||
title: const Text("Shipment & Tracking"),
|
||||
elevation: 0.8,
|
||||
),
|
||||
body: loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(16 * scale),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Order ID: ${data['order_id']}"),
|
||||
Text("Shipment Status: ${data['shipment_status']}"),
|
||||
Text("Shipment Date: ${data['shipment_date']}"),
|
||||
_headerCard(scale),
|
||||
SizedBox(height: 16 * scale),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
Center(
|
||||
child: Icon(
|
||||
Icons.local_shipping,
|
||||
size: 100,
|
||||
color: Colors.indigo.shade300,
|
||||
_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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------- HEADER ----------------
|
||||
Widget _headerCard(double scale) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(14 * scale),
|
||||
decoration: _boxDecoration(scale),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(10 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(12 * scale),
|
||||
),
|
||||
child: Icon(Icons.local_shipping,
|
||||
color: Colors.blue, size: 28 * scale),
|
||||
),
|
||||
SizedBox(width: 12 * scale),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Order #${trackData['order_id'] ?? '-'}",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 16 * scale),
|
||||
),
|
||||
SizedBox(height: 6 * scale),
|
||||
Text(
|
||||
trackData["shipment_status"] ?? "-",
|
||||
style: TextStyle(color: Colors.black54, fontSize: 13 * scale),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------- SHIPMENT SUMMARY ----------------
|
||||
Widget _shipmentSummary(double scale) {
|
||||
if (shipment == null) {
|
||||
return _simpleCard(scale, "Shipment not created yet");
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.all(16 * scale),
|
||||
decoration: _boxDecoration(scale),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Shipment Summary",
|
||||
style:
|
||||
TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
|
||||
SizedBox(height: 12 * scale),
|
||||
|
||||
// Shipment ID
|
||||
Container(
|
||||
padding: EdgeInsets.all(12 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(12 * scale),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.qr_code_2, color: Colors.blue, size: 22 * scale),
|
||||
SizedBox(width: 10 * scale),
|
||||
Text("Shipment ID: ${shipment!['shipment_id']}",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 15 * scale)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 14 * scale),
|
||||
_twoCol("Status", shipment!['status'], scale),
|
||||
_twoCol("Shipment Date", _fmt(shipment!['shipment_date']), scale),
|
||||
_twoCol("Origin", shipment!['origin'], scale),
|
||||
_twoCol("Destination", shipment!['destination'], scale),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _simpleCard(double scale, String text) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(14 * scale),
|
||||
decoration: _boxDecoration(scale),
|
||||
child: Text(text,
|
||||
style: TextStyle(fontSize: 15 * scale, color: Colors.grey.shade700)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _twoCol(String title, dynamic value, double scale) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 6 * scale),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(title,
|
||||
style: TextStyle(color: Colors.grey, fontSize: 13 * scale)),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value?.toString() ?? "-",
|
||||
textAlign: TextAlign.right,
|
||||
style:
|
||||
TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * scale),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------- 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;
|
||||
|
||||
return Container(
|
||||
padding:
|
||||
EdgeInsets.symmetric(vertical: 10 * scale, horizontal: 14 * scale),
|
||||
decoration: BoxDecoration(
|
||||
gradient: delivered
|
||||
? const LinearGradient(colors: [Colors.green, Colors.lightGreen])
|
||||
: const LinearGradient(colors: [Colors.blue, Colors.purple]),
|
||||
borderRadius: BorderRadius.circular(40 * scale),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
delivered ? Icons.verified : Icons.local_shipping,
|
||||
color: Colors.white,
|
||||
size: 16 * scale,
|
||||
),
|
||||
SizedBox(width: 8 * scale),
|
||||
Text(
|
||||
(trackData["shipment_status"] ?? "-")
|
||||
.toString()
|
||||
.toUpperCase(),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13 * scale),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------- PROGRESS BAR ----------------
|
||||
Widget _progressBar(double scale, double width) {
|
||||
final usableWidth = width - (48 * scale);
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.all(12 * 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,
|
||||
children: [
|
||||
// Background
|
||||
Container(
|
||||
width: usableWidth,
|
||||
height: 10 * scale,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(20 * scale)),
|
||||
),
|
||||
|
||||
// Progress Bar
|
||||
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),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// 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);
|
||||
|
||||
return Positioned(
|
||||
left: (usableWidth - 26 * scale) * progress,
|
||||
top: -6 * scale + bob,
|
||||
child: Container(
|
||||
width: 26 * scale,
|
||||
height: 26 * scale,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF4F8CFF), Color(0xFF8A4DFF)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8 * scale),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.local_shipping,
|
||||
size: 16 * scale,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
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) {
|
||||
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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _detailsRow(String title, dynamic value, double scale) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700, fontSize: 14 * scale))),
|
||||
Expanded(
|
||||
child: Text(value?.toString() ?? "-",
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(color: Colors.black87, fontSize: 14 * scale))),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------- BOX DECORATION ----------------
|
||||
BoxDecoration _boxDecoration(double scale) {
|
||||
return BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12.withOpacity(0.05),
|
||||
blurRadius: 10 * scale,
|
||||
offset: Offset(0, 4 * scale),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,52 +16,124 @@ class _OtpScreenState extends State<OtpScreen> {
|
||||
final otpController = TextEditingController();
|
||||
bool verifying = false;
|
||||
|
||||
static const String defaultOtp = '123456'; // default OTP as you said
|
||||
static const String defaultOtp = '123456';
|
||||
|
||||
void _verifyAndSubmit() async {
|
||||
final entered = otpController.text.trim();
|
||||
|
||||
if (entered.length != 6) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Enter 6 digit OTP')));
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(content: Text('Enter 6 digit OTP')));
|
||||
return;
|
||||
}
|
||||
|
||||
if (entered != defaultOtp) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Invalid OTP')));
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(content: Text('Invalid OTP')));
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => verifying = true);
|
||||
|
||||
// send signup payload to backend
|
||||
final res = await RequestService(context).sendSignup(widget.signupPayload);
|
||||
|
||||
setState(() => verifying = false);
|
||||
|
||||
if (res['status'] == true || res['status'] == 'success') {
|
||||
// navigate to waiting screen
|
||||
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const WaitingScreen()));
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const WaitingScreen()));
|
||||
} else {
|
||||
final message = res['message']?.toString() ?? 'Failed';
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text(message)));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pad = MediaQuery.of(context).size.width * 0.06;
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
|
||||
/// 📌 Universal scale factor for responsiveness
|
||||
final scale = (width / 390).clamp(0.85, 1.25);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("OTP Verification")),
|
||||
body: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: pad, vertical: 20),
|
||||
child: Column(children: [
|
||||
const Text("Enter the 6-digit OTP sent to your mobile/email. (Default OTP: 123456)"),
|
||||
const SizedBox(height: 20),
|
||||
RoundedInput(controller: otpController, hint: "Enter OTP", keyboardType: TextInputType.number),
|
||||
const SizedBox(height: 14),
|
||||
PrimaryButton(label: "Verify & Submit", onTap: _verifyAndSubmit, busy: verifying),
|
||||
]),
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
/// 🔙 Back Button
|
||||
Positioned(
|
||||
top: 18 * scale,
|
||||
left: 18 * scale,
|
||||
child: GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: Container(
|
||||
height: 42 * scale,
|
||||
width: 42 * scale,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.indigo,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.arrow_back,
|
||||
color: Colors.white, size: 22 * scale),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
/// 🟦 Center Card
|
||||
Center(
|
||||
child: Container(
|
||||
width: width * 0.90,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 20 * scale, vertical: 28 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 12 * scale,
|
||||
offset: Offset(0, 4 * scale),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"OTP Verification",
|
||||
style: TextStyle(
|
||||
fontSize: 22 * scale,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10 * scale),
|
||||
|
||||
Text(
|
||||
"Enter the 6-digit OTP sent to your mobile/email.\n(Default OTP: 123456)",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 13.5 * scale),
|
||||
),
|
||||
|
||||
SizedBox(height: 18 * scale),
|
||||
|
||||
RoundedInput(
|
||||
controller: otpController,
|
||||
hint: "Enter OTP",
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
|
||||
SizedBox(height: 22 * scale),
|
||||
|
||||
PrimaryButton(
|
||||
label: "Verify & Submit",
|
||||
onTap: _verifyAndSubmit,
|
||||
busy: verifying,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,13 +9,11 @@ import 'login_screen.dart';
|
||||
|
||||
class SettingsScreen extends StatefulWidget {
|
||||
const SettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SettingsScreen> createState() => _SettingsScreenState();
|
||||
}
|
||||
|
||||
class _SettingsScreenState extends State<SettingsScreen> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -27,33 +25,34 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
}
|
||||
|
||||
final profileProvider =
|
||||
Provider.of<UserProfileProvider>(context, listen: false);
|
||||
profileProvider.init(context);
|
||||
|
||||
await profileProvider.loadProfile(context);
|
||||
final profile = Provider.of<UserProfileProvider>(context, listen: false);
|
||||
profile.init(context);
|
||||
await profile.loadProfile(context);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _pickImage() async {
|
||||
try {
|
||||
final picked = await ImagePicker().pickImage(
|
||||
source: ImageSource.gallery,
|
||||
imageQuality: 80,
|
||||
);
|
||||
|
||||
if (picked != null) {
|
||||
final file = File(picked.path);
|
||||
final profileProvider =
|
||||
Provider.of<UserProfileProvider>(context, listen: false);
|
||||
|
||||
profileProvider.init(context);
|
||||
|
||||
final success = await profileProvider.updateProfileImage(context, file);
|
||||
final profile = Provider.of<UserProfileProvider>(context, listen: false);
|
||||
profile.init(context);
|
||||
final ok = await profile.updateProfileImage(context, file);
|
||||
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(success ? "Profile updated" : "Failed to update")),
|
||||
SnackBar(content: Text(ok ? "Profile updated" : "Failed to update")),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text("Error: $e")));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _logout() async {
|
||||
@@ -62,131 +61,309 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
title: const Text("Logout"),
|
||||
content: const Text("Are you sure you want to logout?"),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("Cancel")),
|
||||
TextButton(
|
||||
child: const Text("Cancel"),
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
),
|
||||
TextButton(
|
||||
child: const Text("Logout", style: TextStyle(color: Colors.red)),
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
),
|
||||
child: const Text("Logout", style: TextStyle(color: Colors.red))),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirm == true) {
|
||||
await auth.logout(context);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const LoginScreen()),
|
||||
(route) => false,
|
||||
(r) => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final profileProvider = Provider.of<UserProfileProvider>(context);
|
||||
|
||||
if (profileProvider.loading || profileProvider.profile == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
// ------------------------- REUSABLE FIELD ROW -------------------------
|
||||
Widget _fieldRow(IconData icon, String label, String value, double scale) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12 * scale),
|
||||
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Icon(icon, size: 26 * scale, color: Colors.blueGrey.shade700),
|
||||
SizedBox(width: 14 * scale),
|
||||
Expanded(
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(label,
|
||||
style: TextStyle(
|
||||
fontSize: 14 * scale,
|
||||
color: Colors.grey[700],
|
||||
fontWeight: FontWeight.w700)),
|
||||
SizedBox(height: 4 * scale),
|
||||
Text(value,
|
||||
style: TextStyle(
|
||||
fontSize: 16 * scale, fontWeight: FontWeight.bold)),
|
||||
]),
|
||||
)
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
final p = profileProvider.profile!;
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(18),
|
||||
child: Column(
|
||||
// ------------------------- INFO TILE -------------------------
|
||||
Widget _infoTile(IconData icon, String title, String value, double scale) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// ------------------ PROFILE IMAGE ------------------
|
||||
Center(
|
||||
child: GestureDetector(
|
||||
onTap: _pickImage,
|
||||
child: CircleAvatar(
|
||||
radius: 55,
|
||||
backgroundImage: p.profileImage != null
|
||||
? NetworkImage(p.profileImage!)
|
||||
: null,
|
||||
child: p.profileImage == null
|
||||
? const Icon(Icons.person, size: 55)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 25),
|
||||
|
||||
// ------------------ PROFILE INFO ------------------
|
||||
_infoRow("Customer ID", p.customerId),
|
||||
_infoRow("Name", p.customerName),
|
||||
_infoRow("Company", p.companyName),
|
||||
_infoRow("Email", p.email),
|
||||
_infoRow("Mobile", p.mobile),
|
||||
_infoRow("Address", p.address ?? "Not provided"),
|
||||
_infoRow("Pincode", p.pincode ?? "Not provided"),
|
||||
_infoRow("Status", p.status ?? "N/A"),
|
||||
_infoRow("Customer Type", p.customerType ?? "N/A"),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
Center(
|
||||
child: ElevatedButton(
|
||||
child: const Text("Edit Profile"),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const EditProfileScreen()),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// ------------------ LOGOUT BUTTON ------------------
|
||||
Center(
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 14),
|
||||
),
|
||||
icon: const Icon(Icons.logout, color: Colors.white),
|
||||
label: const Text(
|
||||
"Logout",
|
||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
onPressed: _logout,
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
|
||||
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _infoRow(String title, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 14),
|
||||
Icon(icon, size: 26 * scale, color: Colors.orange.shade800),
|
||||
SizedBox(width: 14 * scale),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
style: TextStyle(
|
||||
fontSize: 13 * scale,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey)),
|
||||
const SizedBox(height: 4),
|
||||
Text(value, style: const TextStyle(fontSize: 16)),
|
||||
color: Colors.black54)),
|
||||
SizedBox(height: 4 * scale),
|
||||
Text(value,
|
||||
style: TextStyle(
|
||||
fontSize: 17 * scale, fontWeight: FontWeight.bold))
|
||||
]),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final profile = Provider.of<UserProfileProvider>(context);
|
||||
|
||||
if (profile.loading || profile.profile == null) {
|
||||
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||
}
|
||||
|
||||
// ----------- RESPONSIVE SCALE -----------
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final scale = (width / 390).clamp(0.80, 1.25);
|
||||
|
||||
final p = profile.profile!;
|
||||
final img = p.profileImage;
|
||||
final name = p.customerName ?? "Unknown";
|
||||
final email = p.email ?? "Not provided";
|
||||
final status = p.status ?? "Active";
|
||||
final cid = p.customerId ?? "—";
|
||||
final company = p.companyName ?? "—";
|
||||
final type = p.customerType ?? "—";
|
||||
final mobile = p.mobile ?? "—";
|
||||
final address = p.address ?? "Not provided";
|
||||
final pincode = p.pincode ?? "—";
|
||||
final isPartner = type.toLowerCase().contains("partner");
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFE9F2FF),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(18 * scale),
|
||||
child: Column(
|
||||
children: [
|
||||
// -------------------- PROFILE SECTION --------------------
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: _pickImage,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 64 * scale,
|
||||
backgroundColor: Colors.grey[200],
|
||||
backgroundImage:
|
||||
img != null ? NetworkImage(img) : null,
|
||||
child: img == null
|
||||
? Icon(Icons.person,
|
||||
size: 70 * scale, color: Colors.grey[600])
|
||||
: null,
|
||||
),
|
||||
|
||||
// ------------------ FIXED STATUS BADGE ------------------
|
||||
Positioned(
|
||||
bottom: 8 * scale,
|
||||
right: 8 * scale,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 12 * scale,
|
||||
vertical: 6 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: status.toLowerCase() == 'active'
|
||||
? Colors.green
|
||||
: Colors.orange,
|
||||
borderRadius: BorderRadius.circular(20 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 8 * scale)
|
||||
],
|
||||
),
|
||||
child: Text(
|
||||
status,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 14 * scale),
|
||||
Text(name,
|
||||
style: TextStyle(
|
||||
fontSize: 20 * scale,
|
||||
fontWeight: FontWeight.bold)),
|
||||
SizedBox(height: 6 * scale),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.email,
|
||||
size: 18 * scale, color: Colors.blueGrey),
|
||||
SizedBox(width: 8 * scale),
|
||||
Text(email,
|
||||
style: TextStyle(
|
||||
fontSize: 14 * scale,
|
||||
color: Colors.grey[700])),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 26 * scale),
|
||||
|
||||
// ---------------------- YELLOW SUMMARY CARD ----------------------
|
||||
Container(
|
||||
padding: EdgeInsets.all(18 * scale),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFFFFF8A3), Color(0xFFFFE275)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.orange.withOpacity(0.25),
|
||||
blurRadius: 14 * scale,
|
||||
offset: Offset(0, 8 * scale))
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_infoTile(Icons.badge, "Customer ID", cid, scale),
|
||||
Divider(height: 30 * scale),
|
||||
_infoTile(Icons.business, "Company Name", company, scale),
|
||||
Divider(height: 30 * scale),
|
||||
_infoTile(Icons.category, "Customer Type", type, scale),
|
||||
SizedBox(height: 20 * scale),
|
||||
|
||||
if (isPartner)
|
||||
Container(
|
||||
padding: EdgeInsets.all(14 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 10 * scale)
|
||||
],
|
||||
),
|
||||
child: Row(children: [
|
||||
Icon(Icons.workspace_premium,
|
||||
size: 32 * scale, color: Colors.amber[800]),
|
||||
SizedBox(width: 12 * scale),
|
||||
Text("Partner",
|
||||
style: TextStyle(
|
||||
fontSize: 16 * scale,
|
||||
fontWeight: FontWeight.bold)),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
),
|
||||
|
||||
SizedBox(height: 24 * scale),
|
||||
|
||||
// ---------------------- DETAILS CARD ----------------------
|
||||
Container(
|
||||
padding: EdgeInsets.all(16 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.06),
|
||||
blurRadius: 12 * scale)
|
||||
]),
|
||||
child: Column(
|
||||
children: [
|
||||
_fieldRow(Icons.phone_android, "Mobile", mobile, scale),
|
||||
const Divider(),
|
||||
_fieldRow(Icons.location_on, "Address", address, scale),
|
||||
const Divider(),
|
||||
_fieldRow(Icons.local_post_office, "Pincode", pincode, scale),
|
||||
SizedBox(height: 20 * scale),
|
||||
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) =>
|
||||
const EditProfileScreen()));
|
||||
},
|
||||
icon: Icon(Icons.edit,
|
||||
color: Colors.white, size: 18 * scale),
|
||||
label: Text("Edit Profile",
|
||||
style: TextStyle(
|
||||
color: Colors.white, fontSize: 14 * scale)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue[700],
|
||||
padding:
|
||||
EdgeInsets.symmetric(vertical: 14 * scale),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12 * scale)),
|
||||
),
|
||||
)),
|
||||
SizedBox(width: 12 * scale),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _logout,
|
||||
icon: Icon(Icons.logout,
|
||||
size: 18 * scale, color: Colors.white),
|
||||
label: Text("Logout",
|
||||
style: TextStyle(
|
||||
color: Colors.white, fontSize: 14 * scale)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red[600],
|
||||
padding:
|
||||
EdgeInsets.symmetric(vertical: 14 * scale),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12 * scale)),
|
||||
),
|
||||
)),
|
||||
])
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 30 * scale),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,16 +21,20 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
bool sending = false;
|
||||
|
||||
void _sendOtp() async {
|
||||
// We don't call backend for OTP here per your flow - OTP is default 123456.
|
||||
// Validate minimal
|
||||
if (cName.text.trim().isEmpty || cCompany.text.trim().isEmpty || cEmail.text.trim().isEmpty || cMobile.text.trim().isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please fill the required fields')));
|
||||
if (cName.text.trim().isEmpty ||
|
||||
cCompany.text.trim().isEmpty ||
|
||||
cEmail.text.trim().isEmpty ||
|
||||
cMobile.text.trim().isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Please fill the required fields')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => sending = true);
|
||||
await Future.delayed(const Duration(milliseconds: 600)); // UI feel
|
||||
await Future.delayed(const Duration(milliseconds: 600));
|
||||
setState(() => sending = false);
|
||||
// Navigate to OTP screen with collected data
|
||||
|
||||
final data = {
|
||||
'customer_name': cName.text.trim(),
|
||||
'company_name': cCompany.text.trim(),
|
||||
@@ -40,40 +44,152 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
'address': cAddress.text.trim(),
|
||||
'pincode': cPincode.text.trim(),
|
||||
};
|
||||
Navigator.of(context).push(MaterialPageRoute(builder: (_) => OtpScreen(signupPayload: data)));
|
||||
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => OtpScreen(signupPayload: data),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pad = MediaQuery.of(context).size.width * 0.06;
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Create Account")),
|
||||
backgroundColor: const Color(0xFFE8F0FF), // Same as Login background
|
||||
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: pad),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(children: [
|
||||
const SizedBox(height: 16),
|
||||
RoundedInput(controller: cName, hint: "Customer name"),
|
||||
const SizedBox(height: 12),
|
||||
RoundedInput(controller: cCompany, hint: "Company name"),
|
||||
const SizedBox(height: 12),
|
||||
RoundedInput(controller: cDesignation, hint: "Designation (optional)"),
|
||||
const SizedBox(height: 12),
|
||||
RoundedInput(controller: cEmail, hint: "Email", keyboardType: TextInputType.emailAddress),
|
||||
const SizedBox(height: 12),
|
||||
RoundedInput(controller: cMobile, hint: "Mobile", keyboardType: TextInputType.phone),
|
||||
const SizedBox(height: 12),
|
||||
RoundedInput(controller: cAddress, hint: "Address", maxLines: 3),
|
||||
const SizedBox(height: 12),
|
||||
RoundedInput(controller: cPincode, hint: "Pincode", keyboardType: TextInputType.number),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16),
|
||||
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
/// 🔵 Back Button (scrolls with form)
|
||||
Material(
|
||||
elevation: 6,
|
||||
shape: const CircleBorder(),
|
||||
color: Colors.indigo.shade700,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Icon(Icons.arrow_back, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
PrimaryButton(label: "Send OTP", onTap: _sendOtp, busy: sending),
|
||||
|
||||
/// 📦 White Elevated Signup Box
|
||||
Center(
|
||||
child: Container(
|
||||
width: width * 0.88,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 28, horizontal: 22),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
blurRadius: 18,
|
||||
spreadRadius: 2,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"Create Account",
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.indigo.shade700,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 25),
|
||||
|
||||
_blueInput(cName, "Customer name"),
|
||||
const SizedBox(height: 14),
|
||||
]),
|
||||
|
||||
_blueInput(cCompany, "Company name"),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
_blueInput(cDesignation, "Designation (optional)"),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
_blueInput(
|
||||
cEmail,
|
||||
"Email",
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
_blueInput(
|
||||
cMobile,
|
||||
"Mobile",
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
_blueInput(
|
||||
cAddress,
|
||||
"Address",
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
_blueInput(
|
||||
cPincode,
|
||||
"Pincode",
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
|
||||
const SizedBox(height: 25),
|
||||
|
||||
PrimaryButton(
|
||||
label: "Send OTP",
|
||||
onTap: _sendOtp,
|
||||
busy: sending,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 🔵 Blue soft background input wrapper (same as Login)
|
||||
Widget _blueInput(
|
||||
TextEditingController controller,
|
||||
String hint, {
|
||||
TextInputType? keyboardType,
|
||||
int maxLines = 1,
|
||||
}) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFD8E7FF),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: RoundedInput(
|
||||
controller: controller,
|
||||
hint: hint,
|
||||
// keyboardType: keyboardType,
|
||||
maxLines: maxLines,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
import 'dashboard_screen.dart';
|
||||
import 'main_bottom_nav.dart';
|
||||
import 'welcome_screen.dart';
|
||||
|
||||
@@ -13,61 +12,166 @@ class SplashScreen extends StatefulWidget {
|
||||
State<SplashScreen> createState() => _SplashScreenState();
|
||||
}
|
||||
|
||||
class _SplashScreenState extends State<SplashScreen> {
|
||||
class _SplashScreenState extends State<SplashScreen>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _mainController;
|
||||
late Animation<double> _scaleAnim;
|
||||
late Animation<double> _fadeAnim;
|
||||
|
||||
late AnimationController _floatController;
|
||||
late Animation<double> _floatAnim;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// MAIN splash animation
|
||||
_mainController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1200),
|
||||
);
|
||||
|
||||
_scaleAnim = Tween(begin: 0.6, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _mainController, curve: Curves.easeOutBack),
|
||||
);
|
||||
|
||||
_fadeAnim = Tween(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _mainController, curve: Curves.easeIn),
|
||||
);
|
||||
|
||||
// FLOATING animation (infinite)
|
||||
_floatController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 2),
|
||||
)..repeat(reverse: true);
|
||||
|
||||
_floatAnim = Tween<double>(begin: -10, end: 10).animate(
|
||||
CurvedAnimation(parent: _floatController, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
_mainController.forward();
|
||||
_init();
|
||||
}
|
||||
|
||||
void _init() async {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
Future<void> _init() async {
|
||||
await Future.delayed(const Duration(milliseconds: 700));
|
||||
|
||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||
|
||||
// 🟢 IMPORTANT → WAIT FOR PREFERENCES TO LOAD
|
||||
await auth.init();
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (auth.isLoggedIn) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const MainBottomNav()),
|
||||
);
|
||||
} else {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const WelcomeScreen()),
|
||||
Future.delayed(const Duration(milliseconds: 900), () {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) =>
|
||||
auth.isLoggedIn ? const MainBottomNav() : const WelcomeScreen(),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_mainController.dispose();
|
||||
_floatController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
|
||||
// Responsive scale factor
|
||||
final scale = (width / 430).clamp(0.9, 1.3);
|
||||
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Container(
|
||||
width: size.width * 0.34,
|
||||
height: size.width * 0.34,
|
||||
body: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.blue.shade50,
|
||||
Colors.white,
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Center(
|
||||
child: AnimatedBuilder(
|
||||
animation: _mainController,
|
||||
builder: (_, __) {
|
||||
return Opacity(
|
||||
opacity: _fadeAnim.value,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, _floatAnim.value), // ⭐ Floating animation
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// ⭐ Animated Floating White Circle Logo
|
||||
Transform.scale(
|
||||
scale: _scaleAnim.value,
|
||||
child: AnimatedBuilder(
|
||||
animation: _floatController,
|
||||
builder: (_, __) {
|
||||
return Container(
|
||||
width: width * 0.50 * scale,
|
||||
height: width * 0.50 * scale,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).primaryColor.withOpacity(0.14),
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black
|
||||
.withOpacity(0.08 + (_floatAnim.value.abs() / 200)),
|
||||
blurRadius: 25 * scale,
|
||||
spreadRadius: 4 * scale,
|
||||
offset: Offset(0, 8 * scale),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"K",
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(28 * scale),
|
||||
child: Image.asset(
|
||||
"assets/Images/K.png",
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 22 * scale),
|
||||
|
||||
Text(
|
||||
"Kent Logistics",
|
||||
style: TextStyle(
|
||||
fontSize: 48,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).primaryColor),
|
||||
fontSize: 22 * scale,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.1,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 6 * scale),
|
||||
|
||||
Text(
|
||||
"Delivering Excellence",
|
||||
style: TextStyle(
|
||||
fontSize: 14 * scale,
|
||||
color: Colors.black54,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
const Text("Kent Logistics",
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,37 +1,198 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class WaitingScreen extends StatelessWidget {
|
||||
class WaitingScreen extends StatefulWidget {
|
||||
const WaitingScreen({super.key});
|
||||
|
||||
@override
|
||||
State<WaitingScreen> createState() => _WaitingScreenState();
|
||||
}
|
||||
|
||||
class _WaitingScreenState extends State<WaitingScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _ctrl;
|
||||
late final Animation<double> _anim;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_ctrl = AnimationController(
|
||||
vsync: this, duration: const Duration(milliseconds: 1200));
|
||||
|
||||
_anim = Tween<double>(begin: 0.0, end: pi).animate(
|
||||
CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut));
|
||||
|
||||
_ctrl.repeat();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ctrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Matrix4 _buildTransform(double value) {
|
||||
return Matrix4.identity()
|
||||
..setEntry(3, 2, 0.001)
|
||||
..rotateX(value);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
|
||||
/// ⭐ Universal scaling factor for responsiveness
|
||||
final scale = (width / 390).clamp(0.85, 1.3);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Request Submitted")),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(18.0),
|
||||
child: Center(
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(Icons.hourglass_top, size: 72, color: Theme.of(context).primaryColor),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"Signup request submitted successfully.",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
/// BACK BUTTON
|
||||
Positioned(
|
||||
top: 12 * scale,
|
||||
left: 12 * scale,
|
||||
child: GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: Container(
|
||||
height: 42 * scale,
|
||||
width: 42 * scale,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Color(0xFF0D47A1),
|
||||
Color(0xFF6A1B9A),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
child: Icon(Icons.arrow_back,
|
||||
color: Colors.white, size: 20 * scale),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
/// MAIN WHITE BOX
|
||||
Center(
|
||||
child: Container(
|
||||
width: width * 0.90,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 20 * scale, vertical: 30 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.10),
|
||||
blurRadius: 14 * scale,
|
||||
offset: Offset(0, 4 * scale),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
/// FLIPPING ICON
|
||||
SizedBox(
|
||||
height: 92 * scale,
|
||||
child: AnimatedBuilder(
|
||||
animation: _anim,
|
||||
builder: (context, child) {
|
||||
final angle = _anim.value;
|
||||
final isBack = angle > (pi / 2);
|
||||
|
||||
return Transform(
|
||||
transform: _buildTransform(angle),
|
||||
alignment: Alignment.center,
|
||||
child: Transform(
|
||||
transform: isBack
|
||||
? Matrix4.rotationY(pi)
|
||||
: Matrix4.identity(),
|
||||
alignment: Alignment.center,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
Icons.hourglass_top,
|
||||
size: 72 * scale,
|
||||
color: Colors.indigo,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 16 * scale),
|
||||
|
||||
Text(
|
||||
"Request Submitted",
|
||||
style: TextStyle(
|
||||
fontSize: 22 * scale,
|
||||
fontWeight: FontWeight.w600),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
|
||||
SizedBox(height: 10 * scale),
|
||||
|
||||
Text(
|
||||
"Signup request submitted successfully.",
|
||||
style: TextStyle(
|
||||
fontSize: 18 * scale,
|
||||
fontWeight: FontWeight.w500),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
SizedBox(height: 8 * scale),
|
||||
|
||||
Text(
|
||||
"Please wait up to 24 hours for admin approval. You will receive an email once approved.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 14 * scale),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
|
||||
SizedBox(height: 24 * scale),
|
||||
|
||||
/// BUTTON WITH GRADIENT WRAPPER
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
Color(0xFF0D47A1),
|
||||
Color(0xFF6A1B9A),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12 * scale),
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
Navigator.of(context)
|
||||
.popUntil((route) => route.isFirst);
|
||||
},
|
||||
child: const Padding(padding: EdgeInsets.symmetric(horizontal: 14, vertical: 12), child: Text("Back to Home")),
|
||||
style: ElevatedButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
foregroundColor: Colors.white,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 20 * scale,
|
||||
vertical: 14 * scale),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(12 * scale),
|
||||
),
|
||||
]),
|
||||
),
|
||||
child: Text(
|
||||
"Back to Home",
|
||||
style: TextStyle(fontSize: 16 * scale),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,39 +1,188 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'signup_screen.dart';
|
||||
import 'login_screen.dart';
|
||||
import '../widgets/primary_button.dart';
|
||||
|
||||
class WelcomeScreen extends StatelessWidget {
|
||||
class WelcomeScreen extends StatefulWidget {
|
||||
const WelcomeScreen({super.key});
|
||||
|
||||
@override
|
||||
State<WelcomeScreen> createState() => _WelcomeScreenState();
|
||||
}
|
||||
|
||||
class _WelcomeScreenState extends State<WelcomeScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _fade;
|
||||
late Animation<Offset> _slide;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 900),
|
||||
);
|
||||
|
||||
_fade = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
|
||||
_slide = Tween<Offset>(
|
||||
begin: const Offset(0, -0.2),
|
||||
end: Offset.zero,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeInOutBack,
|
||||
),
|
||||
);
|
||||
|
||||
_controller.forward();
|
||||
|
||||
_controller.addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
_controller.repeat(reverse: false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _shinyWelcomeText() {
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
double shineX = _controller.value % 1;
|
||||
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"Welcome",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 42,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.indigo.shade700,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: ShaderMask(
|
||||
blendMode: BlendMode.srcATop,
|
||||
shaderCallback: (rect) {
|
||||
final pos = shineX * rect.width;
|
||||
|
||||
return LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
stops: [
|
||||
(pos - 50) / rect.width,
|
||||
pos / rect.width,
|
||||
(pos + 50) / rect.width,
|
||||
],
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Colors.blueAccent,
|
||||
Colors.transparent,
|
||||
],
|
||||
).createShader(rect);
|
||||
},
|
||||
child: Text(
|
||||
"Welcome",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 42,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.indigo.shade900,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final w = MediaQuery.of(context).size.width;
|
||||
final height = MediaQuery.of(context).size.height;
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFE8F0FF),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: w * 0.06),
|
||||
padding: EdgeInsets.symmetric(horizontal: width * 0.07),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 28),
|
||||
Align(alignment: Alignment.centerLeft, child: Text("Welcome", style: Theme.of(context).textTheme.headlineSmall)),
|
||||
const SizedBox(height: 12),
|
||||
/// Animated Welcome text
|
||||
SlideTransition(
|
||||
position: _slide,
|
||||
child: _shinyWelcomeText(),
|
||||
),
|
||||
|
||||
SizedBox(height: height * 0.01),
|
||||
|
||||
/// LOGO SECTION
|
||||
Image.asset(
|
||||
'assets/Images/K.png',
|
||||
height: height * 0.28,
|
||||
),
|
||||
|
||||
SizedBox(height: height * 0.015),
|
||||
|
||||
/// Description Text
|
||||
const Text(
|
||||
"Register to access Kent Logistics services. After signup admin will review and approve your request. Approval may take up to 24 hours.",
|
||||
style: TextStyle(fontSize: 15),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
height: 1.4,
|
||||
color: Colors.black87,
|
||||
),
|
||||
const Spacer(),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (_) => const SignupScreen())),
|
||||
child: const SizedBox(width: double.infinity, child: Center(child: Padding(padding: EdgeInsets.all(14.0), child: Text("Create Account")))),
|
||||
style: ElevatedButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton(
|
||||
onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (_) => const LoginScreen())),
|
||||
child: const SizedBox(width: double.infinity, child: Center(child: Padding(padding: EdgeInsets.all(14.0), child: Text("Login")))),
|
||||
style: OutlinedButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))),
|
||||
|
||||
SizedBox(height: height * 0.04),
|
||||
|
||||
/// 🌈 Create Account Button (Gradient)
|
||||
PrimaryButton(
|
||||
label: "Create Account",
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const SignupScreen()),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
SizedBox(height: height * 0.015),
|
||||
|
||||
/// 🌈 Login Button (Gradient)
|
||||
PrimaryButton(
|
||||
label: "Login",
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const LoginScreen()),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
SizedBox(height: height * 0.02),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -4,30 +4,55 @@ class InvoiceService {
|
||||
final Dio dio;
|
||||
InvoiceService(this.dio);
|
||||
|
||||
// -------------------------------------------------------
|
||||
// ⭐ GET ALL INVOICES
|
||||
// -------------------------------------------------------
|
||||
Future<Map<String, dynamic>> getAllInvoices() async {
|
||||
try {
|
||||
final res = await dio.get("/user/invoices");
|
||||
|
||||
print("🔵 ALL INVOICES RESPONSE:");
|
||||
print(res.data);
|
||||
|
||||
return Map<String, dynamic>.from(res.data);
|
||||
} catch (e) {
|
||||
print("❌ ERROR (All Invoices): $e");
|
||||
return {"success": false, "message": e.toString()};
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// ⭐ GET INSTALLMENTS
|
||||
// -------------------------------------------------------
|
||||
Future<Map<String, dynamic>> getInstallments(int invoiceId) async {
|
||||
try {
|
||||
final res = await dio.get("/user/invoice/$invoiceId/installments");
|
||||
final res =
|
||||
await dio.get("/user/invoice/$invoiceId/installments");
|
||||
|
||||
print("🔵 INSTALLMENTS RESPONSE:");
|
||||
print(res.data);
|
||||
|
||||
return Map<String, dynamic>.from(res.data);
|
||||
} catch (e) {
|
||||
print("❌ ERROR (Installments): $e");
|
||||
return {"success": false, "message": e.toString()};
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔵 NEW FUNCTION — Fetch Full Invoice Details
|
||||
// -------------------------------------------------------
|
||||
// ⭐ GET FULL INVOICE DETAILS (PRINT JSON HERE)
|
||||
// -------------------------------------------------------
|
||||
Future<Map<String, dynamic>> getInvoiceDetails(int invoiceId) async {
|
||||
try {
|
||||
final res = await dio.get("/user/invoice/$invoiceId/details");
|
||||
|
||||
print("👇👇👇 INVOICE API RESPONSE START 👇👇👇");
|
||||
print(res.data); // <-- THIS IS WHAT YOU NEED
|
||||
print("👆👆👆 INVOICE API RESPONSE END 👆👆👆");
|
||||
|
||||
return Map<String, dynamic>.from(res.data);
|
||||
} catch (e) {
|
||||
print("❌ ERROR (Invoice Details): $e");
|
||||
return {"success": false, "message": e.toString()};
|
||||
}
|
||||
}
|
||||
|
||||
315
lib/widgets/invoice_detail_view.dart
Normal file
315
lib/widgets/invoice_detail_view.dart
Normal file
@@ -0,0 +1,315 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class InvoiceDetailView extends StatefulWidget {
|
||||
final Map invoice;
|
||||
const InvoiceDetailView({super.key, required this.invoice});
|
||||
|
||||
@override
|
||||
State<InvoiceDetailView> createState() => _InvoiceDetailViewState();
|
||||
}
|
||||
|
||||
class _InvoiceDetailViewState extends State<InvoiceDetailView>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<Offset> _slideAnimation;
|
||||
|
||||
bool s1 = true;
|
||||
bool s2 = false;
|
||||
bool s3 = false;
|
||||
bool s4 = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 350),
|
||||
);
|
||||
|
||||
_slideAnimation = Tween(begin: const Offset(0, -0.1), end: Offset.zero)
|
||||
.animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
|
||||
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// HEADER SUMMARY 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
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.blue.shade400, Colors.indigo.shade600],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: 8 * scale,
|
||||
offset: Offset(0, 3 * scale),
|
||||
color: Colors.indigo.withOpacity(.3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Invoice #${invoice['invoice_number']}",
|
||||
style: TextStyle(
|
||||
fontSize: 20 * scale,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4 * scale),
|
||||
Text(
|
||||
"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,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// SECTION HEADER (Closer Spacing)
|
||||
// ------------------------------------------------------------------
|
||||
Widget sectionHeader(
|
||||
String title, IconData icon, bool expanded, Function() tap, double scale) {
|
||||
return GestureDetector(
|
||||
onTap: tap,
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(12 * scale), // tighter
|
||||
margin: EdgeInsets.only(bottom: 8 * scale), // closer
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.indigo.shade500, Colors.blue.shade400]),
|
||||
borderRadius: BorderRadius.circular(12 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: 5 * scale,
|
||||
color: Colors.black.withOpacity(.15),
|
||||
offset: Offset(0, 2 * scale),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: Colors.white, size: 20 * scale),
|
||||
SizedBox(width: 8 * scale),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 15 * scale,
|
||||
color: Colors.white,
|
||||
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),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// SECTION BODY (Closer Spacing)
|
||||
// ------------------------------------------------------------------
|
||||
Widget sectionBody(bool visible, List<Widget> children, double scale) {
|
||||
return AnimatedCrossFade(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
crossFadeState:
|
||||
visible ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
||||
firstChild: const SizedBox.shrink(),
|
||||
secondChild: SlideTransition(
|
||||
position: _slideAnimation,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(14 * scale), // tighter
|
||||
margin: EdgeInsets.only(bottom: 10 * scale), // closer
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(12 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: 7 * scale,
|
||||
offset: Offset(0, 2 * scale),
|
||||
color: Colors.black.withOpacity(.07))
|
||||
],
|
||||
),
|
||||
child: Column(children: children),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// DETAIL ROW (Closer Spacing)
|
||||
// ------------------------------------------------------------------
|
||||
Widget detailRow(IconData icon, String label, dynamic value, double scale) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 5 * scale), // closer
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 18 * scale, color: Colors.blueGrey),
|
||||
SizedBox(width: 8 * scale),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600,
|
||||
fontSize: 13 * scale,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Text(
|
||||
value?.toString() ?? "N/A",
|
||||
textAlign: TextAlign.right,
|
||||
style:
|
||||
TextStyle(fontSize: 14 * scale, fontWeight: FontWeight.bold),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 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
|
||||
// ------------------------------------------------------------------
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final invoice = widget.invoice;
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final scale = (width / 390).clamp(0.85, 1.25);
|
||||
|
||||
return ListView(
|
||||
padding: EdgeInsets.all(14 * scale), // slightly tighter
|
||||
children: [
|
||||
headerCard(invoice, scale),
|
||||
invoiceTimeline(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),
|
||||
sectionBody(s2, [
|
||||
detailRow(Icons.person_outline, "Name", invoice['customer_name'], scale),
|
||||
detailRow(Icons.mail, "Email", invoice['customer_email'], scale),
|
||||
detailRow(Icons.phone, "Phone", invoice['customer_mobile'], scale),
|
||||
detailRow(Icons.location_on, "Address", invoice['customer_address'], scale),
|
||||
], 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 %", invoice['gst_percent'], 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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ class MainAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final profileUrl = profileProvider.profile?.profileImage;
|
||||
|
||||
return AppBar(
|
||||
backgroundColor: Colors.lightGreen,
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0.8,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
automaticallyImplyLeading: false,
|
||||
|
||||
@@ -1,19 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PrimaryButton extends StatelessWidget {
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
final bool busy;
|
||||
const PrimaryButton({super.key, required this.label, required this.onTap, this.busy = false});
|
||||
|
||||
const PrimaryButton({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
this.busy = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
Color(0xFF0D47A1), // Blue
|
||||
Color(0xFF6A1B9A), // Purple
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
|
||||
child: ElevatedButton(
|
||||
onPressed: busy ? null : onTap,
|
||||
style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))),
|
||||
child: busy ? const SizedBox(height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) : Text(label, style: const TextStyle(fontSize: 16)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors
|
||||
.transparent, // IMPORTANT: keep transparent to see gradient
|
||||
shadowColor: Colors.transparent,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
|
||||
child: busy
|
||||
? const SizedBox(
|
||||
height: 18,
|
||||
width: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 16, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,16 +19,27 @@ class RoundedInput extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final radius = BorderRadius.circular(12);
|
||||
|
||||
return TextField(
|
||||
controller: controller,
|
||||
keyboardType: keyboardType,
|
||||
obscureText: obscure,
|
||||
maxLines: maxLines,
|
||||
maxLines: obscure ? 1 : maxLines,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey, // grey input text
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: const Color(0xFFD8E7FF), // light blue background
|
||||
hintText: hint,
|
||||
hintStyle: const TextStyle(
|
||||
color: Colors.grey, // grey hint text
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
border: OutlineInputBorder(borderRadius: radius, borderSide: BorderSide.none),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: radius,
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,9 +7,13 @@
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_linux
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
@@ -7,10 +7,12 @@ import Foundation
|
||||
|
||||
import file_selector_macos
|
||||
import path_provider_foundation
|
||||
import share_plus
|
||||
import shared_preferences_foundation
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
}
|
||||
|
||||
178
pubspec.lock
178
pubspec.lock
@@ -1,6 +1,14 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.7"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -9,6 +17,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
barcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: barcode
|
||||
sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.9"
|
||||
bidi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bidi
|
||||
sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.13"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -137,6 +161,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+5"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -192,6 +224,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.4"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -260,26 +300,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:
|
||||
@@ -308,10 +348,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:
|
||||
@@ -336,8 +376,16 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
path_parsing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_parsing
|
||||
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
@@ -384,6 +432,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
pdf:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pdf
|
||||
sha256: "28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.11.3"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -400,6 +464,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
posix:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: posix
|
||||
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.3"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -408,6 +480,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.5+1"
|
||||
qr:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: qr
|
||||
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
share_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: share_plus
|
||||
sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.1.4"
|
||||
share_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_plus_platform_interface
|
||||
sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.2"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -513,10 +609,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:
|
||||
@@ -525,14 +621,54 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.5"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.2"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.2.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -549,6 +685,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.15.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -557,6 +701,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.1"
|
||||
sdks:
|
||||
dart: ">=3.8.1 <4.0.0"
|
||||
flutter: ">=3.32.0"
|
||||
|
||||
@@ -37,6 +37,10 @@ dependencies:
|
||||
google_fonts: ^4.0.3
|
||||
image_picker: ^1.0.7
|
||||
|
||||
share_plus: ^10.0.0
|
||||
path_provider: ^2.1.2
|
||||
pdf: ^3.11.0
|
||||
|
||||
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
@@ -64,6 +68,8 @@ flutter:
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
assets:
|
||||
- assets/Images/K.png
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
|
||||
@@ -7,8 +7,14 @@
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_windows
|
||||
share_plus
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
Reference in New Issue
Block a user