Your changes

This commit is contained in:
divya abdar
2025-12-11 18:36:11 +05:30
parent 3bf27cc29d
commit 9faf983b95
36 changed files with 4677 additions and 1046 deletions

File diff suppressed because one or more lines are too long

3
.fvmrc Normal file
View File

@@ -0,0 +1,3 @@
{
"flutter": "3.27.1"
}

3
.gitignore vendored
View File

@@ -32,3 +32,6 @@ windows/flutter/ephemeral/
# Logs # Logs
*.log *.log
# FVM Version Cache
.fvm/

BIN
assets/Images/K.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -1,3 +1,3 @@
class ApiConfig { class ApiConfig {
static const String baseUrl = "http://10.207.50.74:8000/api"; static const String baseUrl = "http://192.168.0.100:8000/api";
} }

View File

@@ -64,7 +64,7 @@ class KentApp extends StatelessWidget {
useMaterial3: true, useMaterial3: true,
textTheme: GoogleFonts.interTextTheme(), textTheme: GoogleFonts.interTextTheme(),
colorScheme: ColorScheme.fromSeed(seedColor: Colors.red), colorScheme: ColorScheme.fromSeed(seedColor: Colors.red),
scaffoldBackgroundColor: const Color(0xfff8f6ff), // your light background scaffoldBackgroundColor: const Color(0xFFE8F0FF), // your light background
appBarTheme: const AppBarTheme( appBarTheme: const AppBarTheme(
backgroundColor: Colors.indigo, // FIX backgroundColor: Colors.indigo, // FIX
foregroundColor: Colors.white, // white text + icons foregroundColor: Colors.white, // white text + icons

View File

@@ -4,7 +4,11 @@ import '../services/invoice_service.dart';
class InvoiceInstallmentScreen extends StatefulWidget { class InvoiceInstallmentScreen extends StatefulWidget {
final int invoiceId; final int invoiceId;
const InvoiceInstallmentScreen({super.key, required this.invoiceId});
const InvoiceInstallmentScreen({
super.key,
required this.invoiceId,
});
@override @override
State<InvoiceInstallmentScreen> createState() => State<InvoiceInstallmentScreen> createState() =>
@@ -35,39 +39,188 @@ class _InvoiceInstallmentScreenState extends State<InvoiceInstallmentScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text("Installments")), backgroundColor: Colors.grey.shade100,
appBar: AppBar(
title: const Text("Installments"),
elevation: 1,
),
body: loading body: loading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: installments.isEmpty : installments.isEmpty
? const Center( ? _buildEmptyState()
child: Text("Installments not created yet",
style: TextStyle(fontSize: 18)),
)
: ListView.builder( : ListView.builder(
padding: const EdgeInsets.all(16), padding: EdgeInsets.symmetric(
horizontal: width * 0.04,
vertical: 16,
),
itemCount: installments.length, itemCount: installments.length,
itemBuilder: (_, i) { itemBuilder: (_, i) {
final inst = installments[i]; return InstallmentCard(inst: installments[i]);
return Card(
child: ListTile(
title: Text(
"Amount: ₹${inst['amount']?.toString() ?? '0'}"),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Date: ${inst['installment_date'] ?? 'N/A'}"),
Text(
"Payment: ${inst['payment_method'] ?? 'N/A'}"),
Text(
"Reference: ${inst['reference_no'] ?? 'N/A'}"),
],
),
),
);
}, },
), ),
); );
} }
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(
"${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,
),
),
),
],
);
}
} }

View File

@@ -12,250 +12,572 @@ class DashboardScreen extends StatefulWidget {
State<DashboardScreen> createState() => _DashboardScreenState(); State<DashboardScreen> createState() => _DashboardScreenState();
} }
class _DashboardScreenState extends State<DashboardScreen> { class _DashboardScreenState extends State<DashboardScreen>
with TickerProviderStateMixin {
late AnimationController _scaleCtrl;
late AnimationController _shineCtrl;
@override @override
void initState() { void initState() {
super.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 { WidgetsBinding.instance.addPostFrameCallback((_) async {
final auth = Provider.of<AuthProvider>(context, listen: false); final auth = Provider.of<AuthProvider>(context, listen: false);
// STEP 1: Try refresh token BEFORE any API calls
await auth.tryRefreshToken(context); await auth.tryRefreshToken(context);
// STEP 2: Now safe to load dashboard
final dash = Provider.of<DashboardProvider>(context, listen: false); final dash = Provider.of<DashboardProvider>(context, listen: false);
dash.init(context); dash.init(context);
await dash.loadSummary(context); await dash.loadSummary(context);
// STEP 3: Load marks AFTER refresh
final marks = Provider.of<MarkListProvider>(context, listen: false); final marks = Provider.of<MarkListProvider>(context, listen: false);
marks.init(context); marks.init(context);
await marks.loadMarks(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 markCtrl = TextEditingController();
final originCtrl = TextEditingController(); final originCtrl = TextEditingController();
final destCtrl = TextEditingController(); final destCtrl = TextEditingController();
showModalBottomSheet( showDialog(
context: context, context: context,
isScrollControlled: true, barrierDismissible: true,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
builder: (_) { builder: (_) {
return Padding( return Center(
padding: EdgeInsets.fromLTRB( child: Material(
18, color: Colors.transparent,
18, child: Container(
18, width: MediaQuery.of(context).size.width * 0.88,
MediaQuery.of(context).viewInsets.bottom + 20, padding: EdgeInsets.all(20 * scale),
), decoration: BoxDecoration(
child: Column( color: Colors.white,
mainAxisSize: MainAxisSize.min, borderRadius: BorderRadius.circular(20 * scale),
children: [ boxShadow: [
const Text( BoxShadow(
"Add Mark No", color: Colors.black.withOpacity(0.2),
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), blurRadius: 16,
offset: const Offset(0, 6),
),
],
), ),
const SizedBox(height: 16), child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Add Mark No",
style: TextStyle(
fontSize: 20 * scale,
fontWeight: FontWeight.bold,
color: Colors.indigo,
),
),
TextField(controller: markCtrl, decoration: const InputDecoration(labelText: "Mark No")), SizedBox(height: 18 * scale),
const SizedBox(height: 12),
TextField(controller: originCtrl, decoration: const InputDecoration(labelText: "Origin")), _inputField(markCtrl, "Mark No", scale),
const SizedBox(height: 12), SizedBox(height: 12 * scale),
_inputField(originCtrl, "Origin", scale),
SizedBox(height: 12 * scale),
_inputField(destCtrl, "Destination", scale),
TextField(controller: destCtrl, decoration: const InputDecoration(labelText: "Destination")), SizedBox(height: 22 * scale),
const SizedBox(height: 20),
ElevatedButton( SizedBox(
onPressed: () async { width: double.infinity,
final mark = markCtrl.text.trim(); child: DecoratedBox(
final origin = originCtrl.text.trim(); decoration: BoxDecoration(
final dest = destCtrl.text.trim(); 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) { if (mark.isEmpty || origin.isEmpty || dest.isEmpty) {
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context).showSnackBar(
.showSnackBar(const SnackBar(content: Text("All fields required"))); const SnackBar(content: Text("All fields are required")),
return; );
} return;
}
final provider = Provider.of<MarkListProvider>(context, listen: false); final provider =
final res = await provider.addMark(context, mark, origin, dest); Provider.of<MarkListProvider>(context, listen: false);
final res =
await provider.addMark(context, mark, origin, dest);
if (res['success'] == true) { if (res['success'] == true) {
Navigator.pop(context); await Provider.of<MarkListProvider>(context,
} else { listen: false)
final msg = res['message'] ?? "Failed"; .loadMarks(context);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg))); Navigator.pop(context);
} } else {
}, ScaffoldMessenger.of(context).showSnackBar(
child: const Text("Submit"), SnackBar(content: Text(res['message'] ?? "Failed")),
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
padding: EdgeInsets.symmetric(vertical: 14 * scale),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12 * scale),
),
),
child: Text(
"Submit",
style: TextStyle(
fontSize: 16 * scale,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
),
SizedBox(height: 6 * scale),
],
), ),
], ),
), ),
); );
}, },
); );
} }
// ============================================================
// MAIN UI (Responsive)
// ============================================================
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final auth = Provider.of<AuthProvider>(context); final auth = Provider.of<AuthProvider>(context);
final dash = Provider.of<DashboardProvider>(context); final dash = Provider.of<DashboardProvider>(context);
final marks = Provider.of<MarkListProvider>(context); final marks = Provider.of<MarkListProvider>(context);
final name = auth.user?['customer_name'] ?? 'User'; final name = auth.user?['customer_name'] ?? "User";
final width = MediaQuery.of(context).size.width;
if (dash.loading) { final scale = (width / 430).clamp(0.88, 1.08);
return const Center(child: CircularProgressIndicator());
}
return SingleChildScrollView( return SingleChildScrollView(
padding: const EdgeInsets.all(18), padding: EdgeInsets.all(16 * scale),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// HEADER // WELCOME CARD WITH ANIMATION
Text( AnimatedBuilder(
"Welcome, $name 👋", animation: _scaleCtrl,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600), builder: (_, __) {
return Transform.scale(
scale: _scaleCtrl.value,
child: Stack(
children: [
Container(
padding: EdgeInsets.all(20 * scale),
width: double.infinity,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFFFFA726), Color(0xFFFFEB3B)],
),
borderRadius: BorderRadius.circular(18 * scale),
),
child: Text(
"Welcome, $name 👋",
style: TextStyle(
fontSize: 24 * scale,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
// Shine Animation
AnimatedBuilder(
animation: _shineCtrl,
builder: (_, __) {
final left = _shineCtrl.value *
(MediaQuery.of(context).size.width + 140) -
140;
return Positioned(
left: left,
top: -40 * scale,
bottom: -40 * scale,
child: Transform.rotate(
angle: -0.45,
child: Container(
width: 120 * scale,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.white.withOpacity(0),
Colors.white.withOpacity(.3),
Colors.white.withOpacity(0),
],
),
),
),
),
);
},
)
],
),
);
},
), ),
const SizedBox(height: 20),
// ORDER SUMMARY SizedBox(height: 18 * scale),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, _summarySection(
children: [ dash,
_statBox("Active", dash.activeOrders, Colors.blue), rawAmount: "${dash.totalRaw ?? 0}",
_statBox("In Transit", dash.inTransitOrders, Colors.orange), scale: scale,
_statBox("Delivered", dash.deliveredOrders, Colors.green),
],
), ),
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: 26 * scale),
// ADD + VIEW ALL BUTTONS SIDE BY SIDE // MARK LIST CARD
Row( Container(
mainAxisAlignment: MainAxisAlignment.spaceBetween, padding: EdgeInsets.all(16 * scale),
children: [ decoration: BoxDecoration(
ElevatedButton.icon( color: Colors.white,
icon: const Icon(Icons.add), borderRadius: BorderRadius.circular(14 * scale),
label: const Text("Add Mark No"), boxShadow: [
onPressed: _showAddMarkForm, BoxShadow(
), color: Colors.black.withOpacity(.06),
blurRadius: 8 * scale,
if (marks.marks.length > 0) ),
TextButton( ],
onPressed: () { ),
Navigator.push( child: Column(
context, crossAxisAlignment: CrossAxisAlignment.start,
MaterialPageRoute(builder: (_) => const MarkListScreen()), children: [
); // Add Mark Button
}, Center(
child: const Text( child: SizedBox(
"View All →", width: width * 0.95,
style: TextStyle( child: DecoratedBox(
fontSize: 16, decoration: BoxDecoration(
color: Colors.indigo, gradient: const LinearGradient(
fontWeight: FontWeight.w600, colors: [Colors.indigo, Colors.deepPurple],
),
borderRadius: BorderRadius.circular(12 * scale),
),
child: ElevatedButton(
onPressed: () => _showAddMarkForm(scale),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
padding: EdgeInsets.symmetric(vertical: 14 * scale),
),
child: Text(
"Add Mark No",
style: TextStyle(
fontSize: 16 * scale,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
), ),
), ),
), ),
SizedBox(height: 16 * scale),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Latest Mark Numbers",
style: TextStyle(
fontSize: 18 * scale,
fontWeight: FontWeight.bold,
),
),
if (marks.marks.isNotEmpty)
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const MarkListScreen()),
);
},
child: Text(
"View All →",
style: TextStyle(
fontSize: 15 * scale,
color: Colors.indigo,
fontWeight: FontWeight.bold,
),
),
),
],
),
SizedBox(height: 12 * scale),
// Scrollable Mark List
SizedBox(
height: 300 * scale,
child: Scrollbar(
thumbVisibility: true,
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: marks.marks.length,
itemBuilder: (context, i) =>
_markTile(marks.marks[i], scale),
),
),
),
],
),
),
SizedBox(height: 40 * scale),
],
),
);
}
// ============================================================
// SUMMARY SECTION
// ============================================================
Widget _summarySection(dash,
{required String rawAmount, required double scale}) {
return Container(
padding: EdgeInsets.all(14 * scale),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14 * scale),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(.06),
blurRadius: 8 * scale,
),
],
),
child: Column(
children: [
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),
], ],
), ),
const SizedBox(height: 20), SizedBox(height: 12 * scale),
// MARK LIST (only 10 latest) Row(
const Text( mainAxisAlignment: MainAxisAlignment.spaceBetween,
"Latest Mark Numbers", children: [
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), _summaryTile("Delivered", dash.deliveredOrders ?? 0,
Colors.green, Icons.check_circle, scale),
_summaryTile("Total Value", "${dash.totalValue ?? 0}",
Colors.teal, Icons.money, scale),
],
), ),
const SizedBox(height: 10), SizedBox(height: 16 * scale),
if (marks.loading) _rawAmountTile("Raw Amount", rawAmount, scale),
const Center(child: CircularProgressIndicator()) ],
else ),
Column( );
children: List.generate( }
marks.marks.length > 10 ? 10 : marks.marks.length,
(i) { Widget _summaryTile(String title, dynamic value, Color color,
final m = marks.marks[i]; IconData icon, double scale) {
return Card( return Container(
child: ListTile( width: MediaQuery.of(context).size.width * 0.41,
title: Text(m['mark_no']), padding: EdgeInsets.all(14 * scale),
subtitle: Text("${m['origin']}${m['destination']}"), decoration: BoxDecoration(
trailing: Text( color: color.withOpacity(.12),
m['status'], borderRadius: BorderRadius.circular(14 * scale),
style: const TextStyle(color: Colors.indigo), ),
), 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,
),
),
),
],
),
Icon(icon, size: 28 * scale, color: color),
],
),
);
}
Widget _rawAmountTile(String title, String value, double scale) {
return Container(
padding: EdgeInsets.all(14 * scale),
decoration: BoxDecoration(
color: Colors.deepPurple.shade50,
borderRadius: BorderRadius.circular(14 * scale),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
value,
style: TextStyle(
fontSize: 20 * scale,
color: Colors.deepPurple,
fontWeight: FontWeight.bold,
),
),
Text(
title,
style: TextStyle(
fontSize: 13 * scale,
color: Colors.grey,
),
),
],
),
Icon(Icons.currency_rupee,
size: 28 * scale, color: Colors.deepPurple),
],
),
);
}
// ============================================================
// 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,
), ),
), ),
const SizedBox(height: 30),
],
),
);
}
// UI WIDGETS
Widget _statBox(String title, int value, Color color) {
return Container(
width: 110,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
child: Column(
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(
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,
fontWeight: FontWeight.bold,
color: Colors.indigo,
),
), ),
], ],
), ),
); );
} }
// ============================================================
// 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),
),
);
}
} }

View File

@@ -20,8 +20,8 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final profile = Provider.of<UserProfileProvider>( final profile =
context, listen: false).profile; Provider.of<UserProfileProvider>(context, listen: false).profile;
nameCtrl.text = profile?.customerName ?? ''; nameCtrl.text = profile?.customerName ?? '';
companyCtrl.text = profile?.companyName ?? ''; companyCtrl.text = profile?.companyName ?? '';
@@ -32,8 +32,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
} }
Future<void> _submit() async { Future<void> _submit() async {
final provider = final provider = Provider.of<UserProfileProvider>(context, listen: false);
Provider.of<UserProfileProvider>(context, listen: false);
final data = { final data = {
"customer_name": nameCtrl.text, "customer_name": nameCtrl.text,
@@ -44,14 +43,16 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
"pincode": pincodeCtrl.text, "pincode": pincodeCtrl.text,
}; };
final success = final success = await provider.sendProfileUpdateRequest(context, data);
await provider.sendProfileUpdateRequest(context, data);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(success content: Text(
success
? "Request submitted. Wait for admin approval." ? "Request submitted. Wait for admin approval."
: "Failed to submit request")), : "Failed to submit request",
),
),
); );
if (success) Navigator.pop(context); if (success) Navigator.pop(context);
@@ -59,38 +60,179 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( final darkBlue = const Color(0xFF003B73);
appBar: AppBar(title: const Text("Edit Profile")), final screenWidth = MediaQuery.of(context).size.width;
body: Padding(
padding: const EdgeInsets.all(18),
child: Column(
children: [
_field("Name", nameCtrl),
_field("Company", companyCtrl),
_field("Email", emailCtrl),
_field("Mobile", mobileCtrl),
_field("Address", addressCtrl),
_field("Pincode", pincodeCtrl),
const SizedBox(height: 20), return Container(
ElevatedButton( decoration: const BoxDecoration(
onPressed: _submit, gradient: LinearGradient(
child: const Text("Submit Update Request"), 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: [
_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),
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,
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( return Padding(
padding: const EdgeInsets.only(bottom: 14), padding: EdgeInsets.only(bottom: 18 * scale),
child: TextField( child: Container(
controller: ctrl, decoration: BoxDecoration(
decoration: InputDecoration( borderRadius: BorderRadius.circular(16 * scale),
labelText: title, boxShadow: [
border: OutlineInputBorder(), 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(
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,
),
),
),
), ),
), ),
); );

View File

@@ -1,6 +1,12 @@
import 'dart:io';
import 'package:flutter/material.dart'; 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/dio_client.dart';
import '../services/invoice_service.dart'; import '../services/invoice_service.dart';
import '../widgets/invoice_detail_view.dart';
class InvoiceDetailScreen extends StatefulWidget { class InvoiceDetailScreen extends StatefulWidget {
final int invoiceId; final int invoiceId;
@@ -20,149 +26,127 @@ class _InvoiceDetailScreenState extends State<InvoiceDetailScreen> {
load(); load();
} }
// -------------------------------------------------------
// ⭐ LOAD INVOICE FROM API
// -------------------------------------------------------
Future<void> load() async { Future<void> load() async {
final service = InvoiceService(DioClient.getInstance(context)); final service = InvoiceService(DioClient.getInstance(context));
final res = await service.getInvoiceDetails(widget.invoiceId); try {
final res = await service.getInvoiceDetails(widget.invoiceId);
if (res['success'] == true) { if (res['success'] == true) {
invoice = res['invoice'] ?? {}; invoice = res['invoice'] ?? {};
} else {
invoice = {};
}
} catch (e) {
invoice = {};
} finally {
if (mounted) setState(() => loading = false);
} }
loading = false;
setState(() {});
} }
/// ---------- REUSABLE ROW ---------- // -------------------------------------------------------
Widget row(String label, dynamic value) { // ⭐ GENERATE + SAVE PDF TO DOWNLOADS FOLDER
return Padding( // (No Permission Needed)
padding: const EdgeInsets.symmetric(vertical: 6), // -------------------------------------------------------
child: Row( Future<File> generatePDF() async {
mainAxisAlignment: MainAxisAlignment.spaceBetween, final pdf = pw.Document();
children: [
Text(label, style: const TextStyle(fontSize: 14, color: Colors.grey)), pdf.addPage(
Expanded( pw.Page(
child: Text( build: (context) => pw.Column(
value?.toString().isNotEmpty == true ? value.toString() : "N/A", crossAxisAlignment: pw.CrossAxisAlignment.start,
textAlign: TextAlign.end, children: [
style: const TextStyle( pw.Text(
fontSize: 15, "INVOICE DETAILS",
fontWeight: FontWeight.w600, 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 1014)
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final scale = (width / 430).clamp(0.88, 1.08);
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text("Invoice Details")), appBar: AppBar(
title: Text(
"Invoice Details",
style: TextStyle(
fontSize: 18 * scale,
fontWeight: FontWeight.w600,
),
),
// ⭐ PDF + SHARE BUTTONS
actions: [
IconButton(
icon: const Icon(Icons.picture_as_pdf),
onPressed: generatePDF,
),
IconButton(
icon: const Icon(Icons.share),
onPressed: sharePDF,
),
],
),
body: loading body: loading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: invoice.isEmpty
/// ================ INVOICE DATA ================ ? Center(
: Padding( child: Text(
padding: const EdgeInsets.all(16), "No Invoice Data Found",
child: ListView( style: TextStyle(
children: [ fontSize: 18 * scale,
/// -------- Invoice Summary -------- fontWeight: FontWeight.w600,
const Text( color: Colors.black87,
"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("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(
icon: const Icon(Icons.picture_as_pdf),
label: const Text("Download PDF"),
onPressed: () {},
style:
ElevatedButton.styleFrom(backgroundColor: Colors.red),
),
const SizedBox(height: 20),
/// -------- Invoice Items --------
if (invoice['items'] != null)
const Text(
"Invoice Items",
style:
TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
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),
),
),
);
}),
],
), ),
)
: Padding(
padding: EdgeInsets.all(12 * scale),
child: InvoiceDetailView(invoice: invoice),
), ),
); );
} }

View File

@@ -2,11 +2,8 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../providers/invoice_installment_screen.dart'; import '../providers/invoice_installment_screen.dart';
import '../providers/invoice_provider.dart'; import '../providers/invoice_provider.dart';
import '../services/dio_client.dart';
import '../services/invoice_service.dart';
import 'invoice_detail_screen.dart'; import 'invoice_detail_screen.dart';
class InvoiceScreen extends StatefulWidget { class InvoiceScreen extends StatefulWidget {
const InvoiceScreen({super.key}); const InvoiceScreen({super.key});
@@ -15,10 +12,11 @@ class InvoiceScreen extends StatefulWidget {
} }
class _InvoiceScreenState extends State<InvoiceScreen> { class _InvoiceScreenState extends State<InvoiceScreen> {
String searchQuery = "";
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
Provider.of<InvoiceProvider>(context, listen: false) Provider.of<InvoiceProvider>(context, listen: false)
.loadInvoices(context); .loadInvoices(context);
@@ -29,78 +27,263 @@ class _InvoiceScreenState extends State<InvoiceScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final provider = Provider.of<InvoiceProvider>(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) { if (provider.loading) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
if (provider.invoices.isEmpty) { // 🔍 Filter invoices based on search query
return const Center( final filteredInvoices = provider.invoices.where((inv) {
child: Text("No invoices found", style: TextStyle(fontSize: 18))); 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( return Column(
padding: const EdgeInsets.all(16), children: [
itemCount: provider.invoices.length, // 🔍 SEARCH BAR
itemBuilder: (_, i) { Container(
final inv = provider.invoices[i]; 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,
),
),
),
return Card( // 📄 LIST OF INVOICES
margin: const EdgeInsets.only(bottom: 12), Expanded(
child: Padding( child: filteredInvoices.isEmpty
padding: const EdgeInsets.all(14), ? Center(
child: Column( child: Text(
crossAxisAlignment: CrossAxisAlignment.start, "No invoices found",
children: [ style: TextStyle(
Text( fontSize: 18 * scale,
"Invoice ${inv['invoice_number'] ?? 'N/A'}", fontWeight: FontWeight.w600,
style: const TextStyle( ),
fontSize: 18, fontWeight: FontWeight.bold), ),
)
: ListView.builder(
padding: EdgeInsets.all(16 * scale),
itemCount: filteredInvoices.length,
itemBuilder: (_, i) {
final inv = filteredInvoices[i];
return Card(
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16 * scale),
), ),
elevation: 3,
const SizedBox(height: 6), margin: EdgeInsets.only(bottom: 14 * scale),
Text("Date: ${inv['invoice_date'] ?? 'N/A'}"), child: Stack(
Text("Status: ${inv['status'] ?? 'N/A'}"),
Text("Amount: ₹${inv['formatted_amount'] ?? '0'}"),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
OutlinedButton( Padding(
child: const Text("Invoice Details"), padding: EdgeInsets.all(16 * scale),
onPressed: () { child: Column(
Navigator.push( crossAxisAlignment: CrossAxisAlignment.start,
context, children: [
MaterialPageRoute( /// Invoice Number
builder: (_) => InvoiceDetailScreen( Text(
invoiceId: inv['invoice_id'], "Invoice ${inv['invoice_number'] ?? 'N/A'}",
style: TextStyle(
fontSize: 20 * scale,
fontWeight: FontWeight.bold,
color: Colors.blue,
), ),
), ),
);
}, SizedBox(height: 8 * scale),
/// 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(
children: [
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']),
),
);
},
),
),
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']),
),
);
},
),
),
],
),
],
),
), ),
OutlinedButton( /// STATUS BADGE
child: const Text("Installments"), Positioned(
onPressed: () { right: 12 * scale,
Navigator.push( top: 12 * scale,
context, child: Container(
MaterialPageRoute( padding: EdgeInsets.symmetric(
builder: (_) => InvoiceInstallmentScreen( horizontal: 10 * scale,
invoiceId: inv['invoice_id'], 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,
),
),
),
),
); );
} }
} }

View File

@@ -26,27 +26,27 @@ class _LoginScreenState extends State<LoginScreen> {
Future<void> _login() async { Future<void> _login() async {
final auth = Provider.of<AuthProvider>(context, listen: false); final auth = Provider.of<AuthProvider>(context, listen: false);
final loginId = cLoginId.text.trim(); final id = cLoginId.text.trim();
final password = cPassword.text.trim(); final pass = cPassword.text.trim();
if (loginId.isEmpty || password.isEmpty) { if (id.isEmpty || pass.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context)
const SnackBar(content: Text('Please enter login id and password')), .showSnackBar(const SnackBar(content: Text("Please fill all fields")));
);
return; return;
} }
final res = await auth.login(context, loginId, password); final res = await auth.login(context, id, pass);
if (res['success'] == true) { if (res['success'] == true) {
if (!mounted) return; if (!mounted) return;
Navigator.of(context).pushReplacement( Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const MainBottomNav()), MaterialPageRoute(builder: (_) => const MainBottomNav()),
); );
} else { } else {
final msg = res['message']?.toString() ?? 'Login failed'; ScaffoldMessenger.of(context).showSnackBar(
if (!mounted) return; SnackBar(content: Text(res['message'] ?? "Login failed")),
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg))); );
} }
} }
@@ -55,38 +55,113 @@ class _LoginScreenState extends State<LoginScreen> {
final auth = Provider.of<AuthProvider>(context); final auth = Provider.of<AuthProvider>(context);
final width = MediaQuery.of(context).size.width; final width = MediaQuery.of(context).size.width;
return Scaffold( /// ⭐ RESPONSIVE SCALE
appBar: AppBar( final scale = (width / 390).clamp(0.85, 1.25);
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const WelcomeScreen()),
);
},
),
title: const Text('Login'),
),
body: Padding( return Scaffold(
padding: EdgeInsets.symmetric(horizontal: width * 0.06, vertical: 20), backgroundColor: const Color(0xFFE8F0FF),
child: Column(
children: [ body: Stack(
RoundedInput( children: [
controller: cLoginId, /// 🔵 Floating Back Button (Responsive Position + Size)
hint: 'Email / Mobile / Customer ID', 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),
),
),
), ),
const SizedBox(height: 12), ),
RoundedInput(
controller: cPassword, /// 📦 Center White Card (Responsive)
hint: 'Password', Center(
obscure: true, child: Container(
width: width * 0.87,
padding: EdgeInsets.symmetric(
vertical: 28 * scale,
horizontal: 20 * scale,
),
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),
),
],
),
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,
),
],
),
), ),
const SizedBox(height: 18), ),
PrimaryButton(label: 'Login', onTap: _login, busy: auth.loading), ],
],
),
), ),
); );
} }

View File

@@ -17,9 +17,7 @@ class MainBottomNavState extends State<MainBottomNav> {
int _currentIndex = 0; int _currentIndex = 0;
void setIndex(int index) { void setIndex(int index) {
setState(() { setState(() => _currentIndex = index);
_currentIndex = index;
});
} }
final List<Widget> _screens = const [ final List<Widget> _screens = const [
@@ -30,26 +28,166 @@ class MainBottomNavState extends State<MainBottomNav> {
SettingsScreen(), 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 @override
Widget build(BuildContext context) { 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( return Scaffold(
appBar: const MainAppBar(), appBar: const MainAppBar(),
body: _screens[_currentIndex], body: _screens[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex, bottomNavigationBar: Padding(
selectedItemColor: Colors.red, padding: EdgeInsets.only(
unselectedItemColor: Colors.black, left: 10 * scale,
type: BottomNavigationBarType.fixed, right: 10 * scale,
onTap: (index) { bottom: 10 * scale,
setState(() => _currentIndex = index); ),
}, child: LayoutBuilder(
items: const [ builder: (context, constraints) {
BottomNavigationBarItem(icon: Icon(Icons.dashboard_outlined), label: "Dashboard"), final totalWidth = constraints.maxWidth;
BottomNavigationBarItem(icon: Icon(Icons.shopping_bag_outlined), label: "Orders"),
BottomNavigationBarItem(icon: Icon(Icons.receipt_long_outlined), label: "Invoice"), // inner width (after padding)
BottomNavigationBarItem(icon: Icon(Icons.chat_bubble_outline), label: "Chat"), final contentWidth = totalWidth - (containerPadding * 2);
BottomNavigationBarItem(icon: Icon(Icons.settings_outlined), label: "Settings"),
], 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,
),
),
],
),
),
),
),
);
}),
),
],
),
);
},
),
), ),
); );
} }

View File

@@ -10,15 +10,13 @@ class MarkListScreen extends StatefulWidget {
} }
class _MarkListScreenState extends State<MarkListScreen> { class _MarkListScreenState extends State<MarkListScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final provider = Provider.of<MarkListProvider>(context, listen: false); final provider = Provider.of<MarkListProvider>(context, listen: false);
provider.init(context); 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) { Widget build(BuildContext context) {
final marks = Provider.of<MarkListProvider>(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( return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar( 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 body: marks.loading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: ListView.builder( : ListView.builder(
padding: const EdgeInsets.all(12), padding: EdgeInsets.all(10 * scale), // smaller padding
itemCount: marks.marks.length, itemCount: marks.marks.length,
itemBuilder: (_, i) { itemBuilder: (_, i) {
final m = marks.marks[i]; final m = marks.marks[i];
return Card( return Container(
child: ListTile( margin: EdgeInsets.only(bottom: 10 * scale), // reduced margin
title: Text(m['mark_no']), padding: EdgeInsets.all(12 * scale), // smaller padding
subtitle: Text("${m['origin']}${m['destination']}"), decoration: BoxDecoration(
trailing: Text( gradient: const LinearGradient(
m['status'], colors: [
style: const TextStyle(color: Colors.indigo), 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,
),
),
),
],
), ),
); );
}, },

View File

@@ -1,8 +1,8 @@
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../services/dio_client.dart'; import '../services/dio_client.dart';
import '../services/order_service.dart'; import '../services/order_service.dart';
class OrderDetailScreen extends StatefulWidget { class OrderDetailScreen extends StatefulWidget {
final String orderId; final String orderId;
const OrderDetailScreen({super.key, required this.orderId}); const OrderDetailScreen({super.key, required this.orderId});
@@ -14,6 +14,7 @@ class OrderDetailScreen extends StatefulWidget {
class _OrderDetailScreenState extends State<OrderDetailScreen> { class _OrderDetailScreenState extends State<OrderDetailScreen> {
bool loading = true; bool loading = true;
Map order = {}; Map order = {};
final Map<String, bool> _expanded = {};
@override @override
void initState() { void initState() {
@@ -33,105 +34,298 @@ class _OrderDetailScreenState extends State<OrderDetailScreen> {
setState(() {}); setState(() {});
} }
Widget _row(String label, dynamic value) { String _initials(String? s) {
return Padding( if (s == null || s.isEmpty) return "I";
padding: const EdgeInsets.symmetric(vertical: 4), final parts = s.split(" ");
child: Row( return parts.take(2).map((e) => e[0].toUpperCase()).join();
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)),
],
),
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final items = order['items'] ?? []; final items = order['items'] ?? [];
final width = MediaQuery.of(context).size.width;
final scale = (width / 430).clamp(0.85, 1.20);
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text("Order Details")), backgroundColor: const Color(0xFFF0F6FF),
appBar: AppBar(
title: const Text("Order Details"),
elevation: 0,
),
body: loading body: loading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: Padding( : Padding(
padding: const EdgeInsets.all(16), padding: EdgeInsets.all(16 * scale),
child: ListView( child: SingleChildScrollView(
children: [ child: Column(
// ---------------- ORDER SUMMARY ---------------- children: [
const Text( _summaryCard(scale),
"Order Summary", SizedBox(height: 18 * scale),
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), _itemsSection(items, scale),
), SizedBox(height: 18 * scale),
const SizedBox(height: 10), _totalsSection(scale),
],
_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),
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']),
],
),
),
);
}),
],
), ),
), ),
); );
} }
// -----------------------------
// 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)),
],
);
}
} }

View File

@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import '../services/dio_client.dart'; import '../services/dio_client.dart';
import '../services/order_service.dart'; import '../services/order_service.dart';
class OrderInvoiceScreen extends StatefulWidget { class OrderInvoiceScreen extends StatefulWidget {
final String orderId; final String orderId;
const OrderInvoiceScreen({super.key, required this.orderId}); const OrderInvoiceScreen({super.key, required this.orderId});
@@ -11,139 +10,406 @@ class OrderInvoiceScreen extends StatefulWidget {
State<OrderInvoiceScreen> createState() => _OrderInvoiceScreenState(); State<OrderInvoiceScreen> createState() => _OrderInvoiceScreenState();
} }
class _OrderInvoiceScreenState extends State<OrderInvoiceScreen> { class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
with SingleTickerProviderStateMixin {
bool loading = true; bool loading = true;
bool controllerInitialized = false;
Map invoice = {}; Map invoice = {};
late AnimationController _controller;
late Animation<Offset> _slideAnimation;
bool s1 = true;
bool s2 = false;
bool s3 = false;
bool s4 = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
initializeController();
load(); 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 { Future<void> load() async {
final service = OrderService(DioClient.getInstance(context)); final service = OrderService(DioClient.getInstance(context));
final res = await service.getInvoice(widget.orderId); final res = await service.getInvoice(widget.orderId);
if (res['success'] == true) { if (res["success"] == true) {
invoice = res['invoice'] ?? {}; invoice = res["invoice"] ?? {};
} }
loading = false; if (mounted) setState(() => loading = false);
setState(() {});
} }
Widget _row(String label, dynamic value) { @override
return Padding( Widget build(BuildContext context) {
padding: const EdgeInsets.symmetric(vertical: 4), final items = invoice["items"] as List? ?? [];
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, final width = MediaQuery.of(context).size.width;
final scale = (width / 430).clamp(0.85, 1.18);
return Scaffold(
appBar: AppBar(
title: const Text("Invoice"),
),
body: loading || !controllerInitialized
? const Center(child: CircularProgressIndicator())
: ListView(
padding: EdgeInsets.all(16 * scale),
children: [ children: [
Text(label, _headerCard(scale),
style: const TextStyle(fontSize: 14, color: Colors.grey)),
Text(value?.toString() ?? "N/A", // SUMMARY SECTION
style: _sectionHeader("Invoice Summary", Icons.receipt, s1, () {
const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), 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,
),
], ],
), ),
); );
} }
@override // ---------------- HEADER CARD ----------------
Widget build(BuildContext context) { Widget _headerCard(double scale) {
final items = invoice['items'] ?? []; 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),
),
)
],
),
);
}
return Scaffold( // ---------------- SECTION HEADER ----------------
appBar: AppBar(title: const Text("Invoice")), Widget _sectionHeader(
body: loading String title, IconData icon, bool expanded, Function toggle, double scale) {
? const Center(child: CircularProgressIndicator()) return GestureDetector(
: Padding( onTap: () => toggle(),
padding: const EdgeInsets.all(16), child: Container(
child: ListView( 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: [ children: [
const Text("Invoice Summary", Icon(icon, color: Colors.white, size: 20 * scale),
style: SizedBox(width: 10 * scale),
TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), Text(title,
const SizedBox(height: 10), style: TextStyle(
fontWeight: FontWeight.bold,
_row("Invoice No", invoice['invoice_number']), fontSize: 15 * scale,
_row("Invoice Date", invoice['invoice_date']), color: Colors.white)),
_row("Due Date", invoice['due_date']), const Spacer(),
_row("Payment Method", invoice['payment_method']), AnimatedRotation(
_row("Reference No", invoice['reference_no']), turns: expanded ? .5 : 0,
_row("Status", invoice['status']), duration: const Duration(milliseconds: 250),
child: Icon(Icons.keyboard_arrow_down,
const Divider(height: 30), color: Colors.white, size: 22 * scale),
)
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"),
),
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,
children: [
Text("Qty: ${item['qty'] ?? 0}"),
Text("Price: ₹${item['price'] ?? 0}"),
],
),
trailing: Text(
"${item['ttl_amount'] ?? 0}",
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.indigo),
),
),
);
}),
], ],
), ),
), ),
); );
} }
// ---------------- 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,
),
)
],
),
);
}
} }

View File

@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../providers/order_provider.dart'; import '../providers/order_provider.dart';
import 'order_detail_screen.dart'; import 'order_detail_screen.dart';
import 'order_shipment_screen.dart';
import 'order_invoice_screen.dart'; import 'order_invoice_screen.dart';
import 'order_track_screen.dart'; import 'order_track_screen.dart';
@@ -14,6 +13,7 @@ class OrdersScreen extends StatefulWidget {
} }
class _OrdersScreenState extends State<OrdersScreen> { class _OrdersScreenState extends State<OrdersScreen> {
String searchQuery = "";
@override @override
void initState() { void initState() {
@@ -31,77 +31,290 @@ class _OrdersScreenState extends State<OrdersScreen> {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
return ListView.builder( final screenWidth = MediaQuery.of(context).size.width;
padding: const EdgeInsets.all(16), final scale = (screenWidth / 420).clamp(0.85, 1.15);
itemCount: provider.orders.length,
itemBuilder: (_, i) {
final o = provider.orders[i];
return Card( // FILTER ORDERS
elevation: 2, final filteredOrders = provider.orders.where((o) {
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), final q = searchQuery.toLowerCase();
child: Padding( return o["order_id"].toString().toLowerCase().contains(q) ||
padding: const EdgeInsets.all(12), o["status"].toString().toLowerCase().contains(q) ||
child: Column( o["description"].toString().toLowerCase().contains(q) ||
crossAxisAlignment: CrossAxisAlignment.start, o["amount"].toString().toLowerCase().contains(q);
children: [ }).toList();
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), return Column(
children: [
Row( // ⭐⭐ WHITE ELEVATED SEARCH BAR ⭐⭐
mainAxisAlignment: MainAxisAlignment.spaceBetween, Container(
children: [ margin: EdgeInsets.fromLTRB(16 * scale, 16 * scale, 16 * scale, 10 * scale),
_btn("Order", () => _openOrderDetails(o['order_id'])), padding: EdgeInsets.symmetric(horizontal: 14 * scale),
_btn("Shipment", () => _openShipment(o['order_id'])), decoration: BoxDecoration(
_btn("Invoice", () => _openInvoice(o['order_id'])), color: Colors.white,
_btn("Track", () => _openTrack(o['order_id'])), 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);
},
),
),
],
); );
} }
Widget _btn(String text, VoidCallback onTap) { // ORDER CARD UI
return InkWell( Widget _orderCard(Map<String, dynamic> o, double scale) {
onTap: onTap, final progress = getProgress(o['status']);
child: Container( final badgeColor = getStatusColor(o['status']);
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration( return Card(
borderRadius: BorderRadius.circular(6), elevation: 3 * scale,
color: Colors.indigo.shade50, color: Colors.white,
margin: EdgeInsets.only(bottom: 16 * scale),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16 * scale),
),
child: Padding(
padding: EdgeInsets.all(16 * scale),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// TOP ROW
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Order #${o['order_id']}",
style: TextStyle(
fontSize: 18 * scale,
fontWeight: FontWeight.bold,
),
),
Container(
padding: EdgeInsets.symmetric(
horizontal: 12 * scale,
vertical: 6 * scale,
),
decoration: BoxDecoration(
color: badgeColor.withOpacity(0.15),
borderRadius: BorderRadius.circular(12 * scale),
),
child: Text(
o['status'],
style: TextStyle(
color: badgeColor,
fontWeight: FontWeight.bold,
fontSize: 13 * scale,
),
),
),
],
),
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),
],
)
],
), ),
child: Text(text, style: const TextStyle(color: Colors.indigo)),
), ),
); );
} }
void _openOrderDetails(String id) { // BUTTON UI
Navigator.push(context, MaterialPageRoute( Widget _btn(IconData icon, String text, Color fg, Color bg,
builder: (_) => OrderDetailScreen(orderId: id))); VoidCallback onTap, double scale) {
return InkWell(
borderRadius: BorderRadius.circular(12 * scale),
onTap: onTap,
child: Container(
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,
),
),
],
),
),
);
} }
void _openShipment(String id) { // NAVIGATION
Navigator.push(context, MaterialPageRoute( void _openOrderDetails(String id) {
builder: (_) => OrderShipmentScreen(orderId: id))); Navigator.push(
context,
MaterialPageRoute(builder: (_) => OrderDetailScreen(orderId: id)),
);
} }
void _openInvoice(String id) { void _openInvoice(String id) {
Navigator.push(context, MaterialPageRoute( Navigator.push(
builder: (_) => OrderInvoiceScreen(orderId: id))); context,
MaterialPageRoute(builder: (_) => OrderInvoiceScreen(orderId: id)),
);
} }
void _openTrack(String id) { void _openTrack(String id) {
Navigator.push(context, MaterialPageRoute( Navigator.push(
builder: (_) => OrderTrackScreen(orderId: id))); 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;
}

View File

@@ -1,8 +1,9 @@
// lib/screens/order_track_screen.dart
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../services/dio_client.dart'; import '../services/dio_client.dart';
import '../services/order_service.dart'; import '../services/order_service.dart';
class OrderTrackScreen extends StatefulWidget { class OrderTrackScreen extends StatefulWidget {
final String orderId; final String orderId;
const OrderTrackScreen({super.key, required this.orderId}); const OrderTrackScreen({super.key, required this.orderId});
@@ -11,54 +12,579 @@ class OrderTrackScreen extends StatefulWidget {
State<OrderTrackScreen> createState() => _OrderTrackScreenState(); State<OrderTrackScreen> createState() => _OrderTrackScreenState();
} }
class _OrderTrackScreenState extends State<OrderTrackScreen> { class _OrderTrackScreenState extends State<OrderTrackScreen>
with TickerProviderStateMixin {
bool loading = true; bool loading = true;
Map data = {};
Map<String, dynamic>? shipment;
Map<String, dynamic> trackData = {};
late final AnimationController progressController;
late final AnimationController shipController;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
load();
}
Future<void> load() async { progressController = AnimationController(
final service = OrderService(DioClient.getInstance(context)); vsync: this,
final res = await service.trackOrder(widget.orderId); duration: const Duration(milliseconds: 900),
);
if (res['success'] == true) { shipController = AnimationController(
data = res['track']; vsync: this,
} duration: const Duration(milliseconds: 1600),
)..repeat(reverse: true);
loading = false; WidgetsBinding.instance.addPostFrameCallback((_) => _loadData());
setState(() {});
} }
@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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final scale = (width / 430).clamp(0.75, 1.25);
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text("Track Order")), appBar: AppBar(
title: const Text("Shipment & Tracking"),
elevation: 0.8,
),
body: loading body: loading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: Padding( : SingleChildScrollView(
padding: const EdgeInsets.all(16), padding: EdgeInsets.all(16 * scale),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text("Order ID: ${data['order_id']}"), _headerCard(scale),
Text("Shipment Status: ${data['shipment_status']}"), SizedBox(height: 16 * scale),
Text("Shipment Date: ${data['shipment_date']}"),
const SizedBox(height: 20), _shipmentSummary(scale),
Center( SizedBox(height: 16 * scale),
child: Icon(
Icons.local_shipping, _shipmentTotals(scale),
size: 100, SizedBox(height: 16 * scale),
color: Colors.indigo.shade300,
), _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),
)
],
);
}
} }

View File

@@ -16,52 +16,124 @@ class _OtpScreenState extends State<OtpScreen> {
final otpController = TextEditingController(); final otpController = TextEditingController();
bool verifying = false; bool verifying = false;
static const String defaultOtp = '123456'; // default OTP as you said static const String defaultOtp = '123456';
void _verifyAndSubmit() async { void _verifyAndSubmit() async {
final entered = otpController.text.trim(); final entered = otpController.text.trim();
if (entered.length != 6) { 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; return;
} }
if (entered != defaultOtp) { if (entered != defaultOtp) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Invalid OTP'))); ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('Invalid OTP')));
return; return;
} }
setState(() => verifying = true); setState(() => verifying = true);
// send signup payload to backend
final res = await RequestService(context).sendSignup(widget.signupPayload); final res = await RequestService(context).sendSignup(widget.signupPayload);
setState(() => verifying = false); setState(() => verifying = false);
if (res['status'] == true || res['status'] == 'success') { if (res['status'] == true || res['status'] == 'success') {
// navigate to waiting screen Navigator.of(context).pushReplacement(
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const WaitingScreen())); MaterialPageRoute(builder: (_) => const WaitingScreen()));
} else { } else {
final message = res['message']?.toString() ?? 'Failed'; final message = res['message']?.toString() ?? 'Failed';
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(message)));
} }
} }
@override @override
Widget build(BuildContext context) { 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( return Scaffold(
appBar: AppBar(title: const Text("OTP Verification")), body: SafeArea(
body: Padding( child: Stack(
padding: EdgeInsets.symmetric(horizontal: pad, vertical: 20), children: [
child: Column(children: [ /// 🔙 Back Button
const Text("Enter the 6-digit OTP sent to your mobile/email. (Default OTP: 123456)"), Positioned(
const SizedBox(height: 20), top: 18 * scale,
RoundedInput(controller: otpController, hint: "Enter OTP", keyboardType: TextInputType.number), left: 18 * scale,
const SizedBox(height: 14), child: GestureDetector(
PrimaryButton(label: "Verify & Submit", onTap: _verifyAndSubmit, busy: verifying), 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,
),
],
),
),
),
],
),
), ),
); );
} }
} }

View File

@@ -9,13 +9,11 @@ import 'login_screen.dart';
class SettingsScreen extends StatefulWidget { class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key}); const SettingsScreen({super.key});
@override @override
State<SettingsScreen> createState() => _SettingsScreenState(); State<SettingsScreen> createState() => _SettingsScreenState();
} }
class _SettingsScreenState extends State<SettingsScreen> { class _SettingsScreenState extends State<SettingsScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -27,32 +25,33 @@ class _SettingsScreenState extends State<SettingsScreen> {
await Future.delayed(const Duration(milliseconds: 100)); await Future.delayed(const Duration(milliseconds: 100));
} }
final profileProvider = final profile = Provider.of<UserProfileProvider>(context, listen: false);
Provider.of<UserProfileProvider>(context, listen: false); profile.init(context);
profileProvider.init(context); await profile.loadProfile(context);
await profileProvider.loadProfile(context);
}); });
} }
Future<void> _pickImage() async { Future<void> _pickImage() async {
final picked = await ImagePicker().pickImage( try {
source: ImageSource.gallery, final picked = await ImagePicker().pickImage(
imageQuality: 80, 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);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(success ? "Profile updated" : "Failed to update")),
); );
if (picked != null) {
final file = File(picked.path);
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(ok ? "Profile updated" : "Failed to update")),
);
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text("Error: $e")));
} }
} }
@@ -62,131 +61,309 @@ class _SettingsScreenState extends State<SettingsScreen> {
final confirm = await showDialog<bool>( final confirm = await showDialog<bool>(
context: context, context: context,
builder: (_) => AlertDialog( builder: (_) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
title: const Text("Logout"), title: const Text("Logout"),
content: const Text("Are you sure you want to logout?"), content: const Text("Are you sure you want to logout?"),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("Cancel")),
TextButton( TextButton(
child: const Text("Cancel"), onPressed: () => Navigator.pop(context, true),
onPressed: () => Navigator.pop(context, false), child: const Text("Logout", style: TextStyle(color: Colors.red))),
),
TextButton(
child: const Text("Logout", style: TextStyle(color: Colors.red)),
onPressed: () => Navigator.pop(context, true),
),
], ],
), ),
); );
if (confirm == true) { if (confirm == true) {
await auth.logout(context); await auth.logout(context);
if (!mounted) return; if (!mounted) return;
Navigator.pushAndRemoveUntil( Navigator.pushAndRemoveUntil(
context, context,
MaterialPageRoute(builder: (_) => const LoginScreen()), MaterialPageRoute(builder: (_) => const LoginScreen()),
(route) => false, (r) => false,
); );
} }
} }
@override // ------------------------- REUSABLE FIELD ROW -------------------------
Widget build(BuildContext context) { Widget _fieldRow(IconData icon, String label, String value, double scale) {
final profileProvider = Provider.of<UserProfileProvider>(context); return Padding(
padding: EdgeInsets.symmetric(vertical: 12 * scale),
if (profileProvider.loading || profileProvider.profile == null) { child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
return const Center(child: CircularProgressIndicator()); Icon(icon, size: 26 * scale, color: Colors.blueGrey.shade700),
} SizedBox(width: 14 * scale),
Expanded(
final p = profileProvider.profile!; child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(label,
return SingleChildScrollView( style: TextStyle(
padding: const EdgeInsets.all(18), fontSize: 14 * scale,
child: Column( color: Colors.grey[700],
crossAxisAlignment: CrossAxisAlignment.start, fontWeight: FontWeight.w700)),
children: [ SizedBox(height: 4 * scale),
// ------------------ PROFILE IMAGE ------------------ Text(value,
Center( style: TextStyle(
child: GestureDetector( fontSize: 16 * scale, fontWeight: FontWeight.bold)),
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) { // ------------------------- INFO TILE -------------------------
return Padding( Widget _infoTile(IconData icon, String title, String value, double scale) {
padding: const EdgeInsets.only(bottom: 14), return Row(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Icon(icon, size: 26 * scale, color: Colors.orange.shade800),
Text(title, SizedBox(width: 14 * scale),
style: const TextStyle( Expanded(
fontSize: 13, child: Column(
fontWeight: FontWeight.w600, crossAxisAlignment: CrossAxisAlignment.start,
color: Colors.grey)), children: [
const SizedBox(height: 4), Text(title,
Text(value, style: const TextStyle(fontSize: 16)), style: TextStyle(
], fontSize: 13 * scale,
fontWeight: FontWeight.w600,
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),
],
),
),
), ),
); );
} }

View File

@@ -21,16 +21,20 @@ class _SignupScreenState extends State<SignupScreen> {
bool sending = false; bool sending = false;
void _sendOtp() async { void _sendOtp() async {
// We don't call backend for OTP here per your flow - OTP is default 123456. if (cName.text.trim().isEmpty ||
// Validate minimal cCompany.text.trim().isEmpty ||
if (cName.text.trim().isEmpty || cCompany.text.trim().isEmpty || cEmail.text.trim().isEmpty || cMobile.text.trim().isEmpty) { cEmail.text.trim().isEmpty ||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please fill the required fields'))); cMobile.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please fill the required fields')),
);
return; return;
} }
setState(() => sending = true); setState(() => sending = true);
await Future.delayed(const Duration(milliseconds: 600)); // UI feel await Future.delayed(const Duration(milliseconds: 600));
setState(() => sending = false); setState(() => sending = false);
// Navigate to OTP screen with collected data
final data = { final data = {
'customer_name': cName.text.trim(), 'customer_name': cName.text.trim(),
'company_name': cCompany.text.trim(), 'company_name': cCompany.text.trim(),
@@ -40,40 +44,152 @@ class _SignupScreenState extends State<SignupScreen> {
'address': cAddress.text.trim(), 'address': cAddress.text.trim(),
'pincode': cPincode.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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final pad = MediaQuery.of(context).size.width * 0.06; final width = MediaQuery.of(context).size.width;
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text("Create Account")), backgroundColor: const Color(0xFFE8F0FF), // Same as Login background
body: SafeArea( body: SafeArea(
child: Padding( child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: pad), child: Padding(
child: SingleChildScrollView( padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16),
child: Column(children: [
const SizedBox(height: 16), child: Column(
RoundedInput(controller: cName, hint: "Customer name"), crossAxisAlignment: CrossAxisAlignment.start,
const SizedBox(height: 12), children: [
RoundedInput(controller: cCompany, hint: "Company name"), /// 🔵 Back Button (scrolls with form)
const SizedBox(height: 12), Material(
RoundedInput(controller: cDesignation, hint: "Designation (optional)"), elevation: 6,
const SizedBox(height: 12), shape: const CircleBorder(),
RoundedInput(controller: cEmail, hint: "Email", keyboardType: TextInputType.emailAddress), color: Colors.indigo.shade700,
const SizedBox(height: 12), child: InkWell(
RoundedInput(controller: cMobile, hint: "Mobile", keyboardType: TextInputType.phone), borderRadius: BorderRadius.circular(30),
const SizedBox(height: 12), onTap: () => Navigator.pop(context),
RoundedInput(controller: cAddress, hint: "Address", maxLines: 3), child: const Padding(
const SizedBox(height: 12), padding: EdgeInsets.all(10),
RoundedInput(controller: cPincode, hint: "Pincode", keyboardType: TextInputType.number), child: Icon(Icons.arrow_back, color: Colors.white),
const SizedBox(height: 20), ),
PrimaryButton(label: "Send OTP", onTap: _sendOtp, busy: sending), ),
const SizedBox(height: 14), ),
]),
const SizedBox(height: 20),
/// 📦 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,
),
);
}
} }

View File

@@ -2,7 +2,6 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../providers/auth_provider.dart'; import '../providers/auth_provider.dart';
import 'dashboard_screen.dart';
import 'main_bottom_nav.dart'; import 'main_bottom_nav.dart';
import 'welcome_screen.dart'; import 'welcome_screen.dart';
@@ -13,61 +12,166 @@ class SplashScreen extends StatefulWidget {
State<SplashScreen> createState() => _SplashScreenState(); 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 @override
void initState() { void initState() {
super.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(); _init();
} }
void _init() async { Future<void> _init() async {
await Future.delayed(const Duration(milliseconds: 500)); await Future.delayed(const Duration(milliseconds: 700));
final auth = Provider.of<AuthProvider>(context, listen: false); final auth = Provider.of<AuthProvider>(context, listen: false);
// 🟢 IMPORTANT → WAIT FOR PREFERENCES TO LOAD
await auth.init(); await auth.init();
if (!mounted) return; if (!mounted) return;
if (auth.isLoggedIn) { Future.delayed(const Duration(milliseconds: 900), () {
Navigator.of(context).pushReplacement( Navigator.pushReplacement(
MaterialPageRoute(builder: (_) => const MainBottomNav()), context,
MaterialPageRoute(
builder: (_) =>
auth.isLoggedIn ? const MainBottomNav() : const WelcomeScreen(),
),
); );
} else { });
Navigator.of(context).pushReplacement( }
MaterialPageRoute(builder: (_) => const WelcomeScreen()),
); @override
} void dispose() {
_mainController.dispose();
_floatController.dispose();
super.dispose();
} }
@override @override
Widget build(BuildContext context) { 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( return Scaffold(
body: Center( body: Container(
child: Column(mainAxisSize: MainAxisSize.min, children: [ decoration: BoxDecoration(
Container( gradient: LinearGradient(
width: size.width * 0.34, colors: [
height: size.width * 0.34, Colors.blue.shade50,
decoration: BoxDecoration( Colors.white,
shape: BoxShape.circle, ],
color: Theme.of(context).primaryColor.withOpacity(0.14), begin: Alignment.topCenter,
), end: Alignment.bottomCenter,
child: Center(
child: Text(
"K",
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor),
),
),
), ),
const SizedBox(height: 18), ),
const Text("Kent Logistics", width: double.infinity,
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)), 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: 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: 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: 22 * scale,
fontWeight: FontWeight.w700,
letterSpacing: 1.1,
),
),
SizedBox(height: 6 * scale),
Text(
"Delivering Excellence",
style: TextStyle(
fontSize: 14 * scale,
color: Colors.black54,
),
)
],
),
),
);
},
),
),
), ),
); );
} }

View File

@@ -1,37 +1,198 @@
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class WaitingScreen extends StatelessWidget { class WaitingScreen extends StatefulWidget {
const WaitingScreen({super.key}); 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 @override
Widget build(BuildContext context) { 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( return Scaffold(
appBar: AppBar(title: const Text("Request Submitted")), body: SafeArea(
body: Padding( child: Stack(
padding: const EdgeInsets.all(18.0), children: [
child: Center( /// BACK BUTTON
child: Column(mainAxisSize: MainAxisSize.min, children: [ Positioned(
Icon(Icons.hourglass_top, size: 72, color: Theme.of(context).primaryColor), top: 12 * scale,
const SizedBox(height: 16), left: 12 * scale,
const Text( child: GestureDetector(
"Signup request submitted successfully.", onTap: () => Navigator.pop(context),
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), child: Container(
textAlign: TextAlign.center, 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),
),
),
), ),
const SizedBox(height: 8),
const Text( /// MAIN WHITE BOX
"Please wait up to 24 hours for admin approval. You will receive an email once approved.", Center(
textAlign: TextAlign.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,
),
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),
),
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);
},
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),
),
),
),
],
),
),
), ),
const SizedBox(height: 24), ],
ElevatedButton(
onPressed: () {
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))),
),
]),
), ),
), ),
); );

View File

@@ -1,39 +1,188 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'signup_screen.dart'; import 'signup_screen.dart';
import 'login_screen.dart'; import 'login_screen.dart';
import '../widgets/primary_button.dart';
class WelcomeScreen extends StatelessWidget { class WelcomeScreen extends StatefulWidget {
const WelcomeScreen({super.key}); 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 @override
Widget build(BuildContext context) { 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( return Scaffold(
backgroundColor: const Color(0xFFE8F0FF),
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
padding: EdgeInsets.symmetric(horizontal: w * 0.06), padding: EdgeInsets.symmetric(horizontal: width * 0.07),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const SizedBox(height: 28), /// Animated Welcome text
Align(alignment: Alignment.centerLeft, child: Text("Welcome", style: Theme.of(context).textTheme.headlineSmall)), SlideTransition(
const SizedBox(height: 12), 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( const Text(
"Register to access Kent Logistics services. After signup admin will review and approve your request. Approval may take up to 24 hours.", "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( SizedBox(height: height * 0.04),
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")))), /// 🌈 Create Account Button (Gradient)
style: ElevatedButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))), PrimaryButton(
label: "Create Account",
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const SignupScreen()),
);
},
), ),
const SizedBox(height: 12),
OutlinedButton( SizedBox(height: height * 0.015),
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")))), /// 🌈 Login Button (Gradient)
style: OutlinedButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))), PrimaryButton(
label: "Login",
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const LoginScreen()),
);
},
), ),
const SizedBox(height: 24),
SizedBox(height: height * 0.02),
], ],
), ),
), ),

View File

@@ -4,30 +4,55 @@ class InvoiceService {
final Dio dio; final Dio dio;
InvoiceService(this.dio); InvoiceService(this.dio);
// -------------------------------------------------------
// ⭐ GET ALL INVOICES
// -------------------------------------------------------
Future<Map<String, dynamic>> getAllInvoices() async { Future<Map<String, dynamic>> getAllInvoices() async {
try { try {
final res = await dio.get("/user/invoices"); final res = await dio.get("/user/invoices");
print("🔵 ALL INVOICES RESPONSE:");
print(res.data);
return Map<String, dynamic>.from(res.data); return Map<String, dynamic>.from(res.data);
} catch (e) { } catch (e) {
print("❌ ERROR (All Invoices): $e");
return {"success": false, "message": e.toString()}; return {"success": false, "message": e.toString()};
} }
} }
// -------------------------------------------------------
// ⭐ GET INSTALLMENTS
// -------------------------------------------------------
Future<Map<String, dynamic>> getInstallments(int invoiceId) async { Future<Map<String, dynamic>> getInstallments(int invoiceId) async {
try { 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); return Map<String, dynamic>.from(res.data);
} catch (e) { } catch (e) {
print("❌ ERROR (Installments): $e");
return {"success": false, "message": e.toString()}; 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 { Future<Map<String, dynamic>> getInvoiceDetails(int invoiceId) async {
try { try {
final res = await dio.get("/user/invoice/$invoiceId/details"); 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); return Map<String, dynamic>.from(res.data);
} catch (e) { } catch (e) {
print("❌ ERROR (Invoice Details): $e");
return {"success": false, "message": e.toString()}; return {"success": false, "message": e.toString()};
} }
} }

View 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),
],
);
}
}

View File

@@ -13,7 +13,7 @@ class MainAppBar extends StatelessWidget implements PreferredSizeWidget {
final profileUrl = profileProvider.profile?.profileImage; final profileUrl = profileProvider.profile?.profileImage;
return AppBar( return AppBar(
backgroundColor: Colors.lightGreen, backgroundColor: Colors.white,
elevation: 0.8, elevation: 0.8,
surfaceTintColor: Colors.transparent, surfaceTintColor: Colors.transparent,
automaticallyImplyLeading: false, automaticallyImplyLeading: false,

View File

@@ -1,19 +1,62 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class PrimaryButton extends StatelessWidget { class PrimaryButton extends StatelessWidget {
final String label; final String label;
final VoidCallback onTap; final VoidCallback onTap;
final bool busy; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton(
onPressed: busy ? null : onTap, child: Container(
style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))), decoration: BoxDecoration(
child: busy ? const SizedBox(height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) : Text(label, style: const TextStyle(fontSize: 16)), 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(
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),
),
),
), ),
); );
} }
} }

View File

@@ -19,16 +19,27 @@ class RoundedInput extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final radius = BorderRadius.circular(12); final radius = BorderRadius.circular(12);
return TextField( return TextField(
controller: controller, controller: controller,
keyboardType: keyboardType, keyboardType: keyboardType,
obscureText: obscure, obscureText: obscure,
maxLines: maxLines, maxLines: obscure ? 1 : maxLines,
style: const TextStyle(
color: Colors.grey, // grey input text
),
decoration: InputDecoration( decoration: InputDecoration(
filled: true, filled: true,
fillColor: const Color(0xFFD8E7FF), // light blue background
hintText: hint, hintText: hint,
hintStyle: const TextStyle(
color: Colors.grey, // grey hint text
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
border: OutlineInputBorder(borderRadius: radius, borderSide: BorderSide.none), border: OutlineInputBorder(
borderRadius: radius,
borderSide: BorderSide.none,
),
), ),
); );
} }

View File

@@ -7,9 +7,13 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <file_selector_linux/file_selector_plugin.h> #include <file_selector_linux/file_selector_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar); 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);
} }

View File

@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux file_selector_linux
url_launcher_linux
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@@ -7,10 +7,12 @@ import Foundation
import file_selector_macos import file_selector_macos
import path_provider_foundation import path_provider_foundation
import share_plus
import shared_preferences_foundation import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
} }

View File

@@ -1,6 +1,14 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
archive:
dependency: transitive
description:
name: archive
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
url: "https://pub.dev"
source: hosted
version: "4.0.7"
async: async:
dependency: transitive dependency: transitive
description: description:
@@ -9,6 +17,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.13.0" 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: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -137,6 +161,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.9.3+5" version: "0.9.3+5"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -192,6 +224,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.2" version: "4.1.2"
image:
dependency: transitive
description:
name: image
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
url: "https://pub.dev"
source: hosted
version: "4.5.4"
image_picker: image_picker:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -260,26 +300,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.9" version: "11.0.2"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_flutter_testing name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.9" version: "3.0.10"
leak_tracker_testing: leak_tracker_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_testing name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.2"
lints: lints:
dependency: transitive dependency: transitive
description: description:
@@ -308,10 +348,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.16.0" version: "1.17.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@@ -336,8 +376,16 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" version: "1.9.1"
path_provider: path_parsing:
dependency: transitive dependency: transitive
description:
name: path_parsing
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
path_provider:
dependency: "direct main"
description: description:
name: path_provider name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
@@ -384,6 +432,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" 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: platform:
dependency: transitive dependency: transitive
description: description:
@@ -400,6 +464,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
posix:
dependency: transitive
description:
name: posix
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
url: "https://pub.dev"
source: hosted
version: "6.0.3"
provider: provider:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -408,6 +480,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.5+1" 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: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -513,10 +609,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.4" version: "0.7.7"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -525,14 +621,54 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" 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: vector_math:
dependency: transitive dependency: transitive
description: description:
name: vector_math name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.2.0"
vm_service: vm_service:
dependency: transitive dependency: transitive
description: description:
@@ -549,6 +685,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "1.1.1"
win32:
dependency: transitive
description:
name: win32
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
url: "https://pub.dev"
source: hosted
version: "5.15.0"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@@ -557,6 +701,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
url: "https://pub.dev"
source: hosted
version: "6.6.1"
sdks: sdks:
dart: ">=3.8.1 <4.0.0" dart: ">=3.8.1 <4.0.0"
flutter: ">=3.32.0" flutter: ">=3.32.0"

View File

@@ -37,6 +37,10 @@ dependencies:
google_fonts: ^4.0.3 google_fonts: ^4.0.3
image_picker: ^1.0.7 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. # 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 # included with your application, so that you can use the icons in
# the material Icons class. # the material Icons class.
uses-material-design: true uses-material-design: true
assets:
- assets/Images/K.png
# To add assets to your application, add an assets section, like this: # To add assets to your application, add an assets section, like this:
# assets: # assets:

View File

@@ -7,8 +7,14 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <file_selector_windows/file_selector_windows.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) { void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar( FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows")); registry->GetRegistrarForPlugin("FileSelectorWindows"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
} }

View File

@@ -4,6 +4,8 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows file_selector_windows
share_plus
url_launcher_windows
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST