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
*.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 {
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,
textTheme: GoogleFonts.interTextTheme(),
colorScheme: ColorScheme.fromSeed(seedColor: Colors.red),
scaffoldBackgroundColor: const Color(0xfff8f6ff), // your light background
scaffoldBackgroundColor: const Color(0xFFE8F0FF), // your light background
appBarTheme: const AppBarTheme(
backgroundColor: Colors.indigo, // FIX
foregroundColor: Colors.white, // white text + icons

View File

@@ -4,7 +4,11 @@ import '../services/invoice_service.dart';
class InvoiceInstallmentScreen extends StatefulWidget {
final int invoiceId;
const InvoiceInstallmentScreen({super.key, required this.invoiceId});
const InvoiceInstallmentScreen({
super.key,
required this.invoiceId,
});
@override
State<InvoiceInstallmentScreen> createState() =>
@@ -35,39 +39,188 @@ class _InvoiceInstallmentScreenState extends State<InvoiceInstallmentScreen> {
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return Scaffold(
appBar: AppBar(title: const Text("Installments")),
backgroundColor: Colors.grey.shade100,
appBar: AppBar(
title: const Text("Installments"),
elevation: 1,
),
body: loading
? const Center(child: CircularProgressIndicator())
: installments.isEmpty
? const Center(
child: Text("Installments not created yet",
style: TextStyle(fontSize: 18)),
)
? _buildEmptyState()
: ListView.builder(
padding: const EdgeInsets.all(16),
padding: EdgeInsets.symmetric(
horizontal: width * 0.04,
vertical: 16,
),
itemCount: installments.length,
itemBuilder: (_, i) {
final inst = installments[i];
return Card(
child: ListTile(
title: Text(
"Amount: ₹${inst['amount']?.toString() ?? '0'}"),
subtitle: Column(
return InstallmentCard(inst: installments[i]);
},
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.receipt_long,
size: 70, color: Colors.grey.shade400),
const SizedBox(height: 12),
Text(
"No Installments Created",
style: TextStyle(
fontSize: 18,
color: Colors.grey.shade600,
),
),
],
),
);
}
}
class InstallmentCard extends StatelessWidget {
final Map inst;
const InstallmentCard({super.key, required this.inst});
String getString(key) => inst[key]?.toString() ?? "N/A";
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final isTablet = width > 600;
final padding = isTablet ? 28.0 : 20.0;
final amountSize = isTablet ? 30.0 : 26.0;
return LayoutBuilder(
builder: (context, constraints) {
return Container(
margin: const EdgeInsets.only(bottom: 18),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Padding(
padding: EdgeInsets.all(padding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Amount + Payment Method Row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Date: ${inst['installment_date'] ?? 'N/A'}"),
Text(
"Payment: ${inst['payment_method'] ?? 'N/A'}"),
Text(
"Reference: ${inst['reference_no'] ?? 'N/A'}"),
"${getString('amount')}",
style: TextStyle(
fontSize: amountSize,
fontWeight: FontWeight.bold,
letterSpacing: 0.2,
),
),
// Payment Chip
Container(
padding: EdgeInsets.symmetric(
vertical: isTablet ? 8 : 6,
horizontal: isTablet ? 16 : 12,
),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(50),
),
child: Text(
getString('payment_method'),
style: TextStyle(
fontWeight: FontWeight.w600,
color: Colors.blue.shade700,
fontSize: isTablet ? 15 : 13.5,
),
),
),
],
),
SizedBox(height: isTablet ? 24 : 18),
// Responsive Info Rows
buildInfoRow(
Icons.calendar_month,
"Date",
getString("installment_date"),
isTablet
),
SizedBox(height: isTablet ? 14 : 10),
buildInfoRow(
Icons.confirmation_number,
"Reference",
getString("reference_no"),
isTablet
),
SizedBox(height: isTablet ? 24 : 18),
Divider(color: Colors.grey.shade300, thickness: 1),
SizedBox(height: isTablet ? 10 : 6),
Align(
alignment: Alignment.centerRight,
child: Text(
"Installment #${inst['id'] ?? ''}",
style: TextStyle(
fontSize: isTablet ? 15 : 13,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
);
},
);
}
/// Responsive info row builder
Widget buildInfoRow(IconData icon, String label, String value, bool isTablet) {
return Row(
children: [
Icon(icon, size: isTablet ? 24 : 20, color: Colors.grey.shade600),
const SizedBox(width: 10),
Text(
"$label:",
style: TextStyle(
fontSize: isTablet ? 17 : 15,
fontWeight: FontWeight.w600,
color: Colors.grey.shade700,
),
),
const SizedBox(width: 6),
Expanded(
child: Text(
value,
style: TextStyle(
fontSize: isTablet ? 16 : 15,
color: Colors.grey.shade800,
fontWeight: FontWeight.w500,
),
),
),
],
);
}
}

View File

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

View File

@@ -20,8 +20,8 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
@override
void initState() {
super.initState();
final profile = Provider.of<UserProfileProvider>(
context, listen: false).profile;
final profile =
Provider.of<UserProfileProvider>(context, listen: false).profile;
nameCtrl.text = profile?.customerName ?? '';
companyCtrl.text = profile?.companyName ?? '';
@@ -32,8 +32,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
}
Future<void> _submit() async {
final provider =
Provider.of<UserProfileProvider>(context, listen: false);
final provider = Provider.of<UserProfileProvider>(context, listen: false);
final data = {
"customer_name": nameCtrl.text,
@@ -44,14 +43,16 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
"pincode": pincodeCtrl.text,
};
final success =
await provider.sendProfileUpdateRequest(context, data);
final success = await provider.sendProfileUpdateRequest(context, data);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success
content: Text(
success
? "Request submitted. Wait for admin approval."
: "Failed to submit request")),
: "Failed to submit request",
),
),
);
if (success) Navigator.pop(context);
@@ -59,38 +60,179 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Edit Profile")),
body: Padding(
padding: const EdgeInsets.all(18),
final darkBlue = const Color(0xFF003B73);
final screenWidth = MediaQuery.of(context).size.width;
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFFB3E5FC), Color(0xFFE1F5FE)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Scaffold(
backgroundColor: Colors.transparent,
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
double scale = (screenWidth / 390).clamp(0.75, 1.3);
return SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 18 * scale),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 8 * scale),
/// BACK BUTTON
IconButton(
icon: Icon(Icons.arrow_back_ios_new, size: 22 * scale),
color: Colors.red,
onPressed: () => Navigator.pop(context),
),
SizedBox(height: 10 * scale),
/// TITLE
Center(
child: Text(
"Edit Profile",
style: TextStyle(
color: darkBlue,
fontSize: 26 * scale,
fontWeight: FontWeight.bold,
),
),
),
SizedBox(height: 20 * scale),
/// RESPONSIVE CENTERED FORM CARD
Center(
child: Container(
width: screenWidth > 650 ? 550 : double.infinity,
padding: EdgeInsets.all(24 * scale),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.95),
borderRadius: BorderRadius.circular(20 * scale),
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(0.15),
blurRadius: 18 * scale,
offset: Offset(0, 6 * scale),
),
],
),
child: Column(
children: [
_field("Name", nameCtrl),
_field("Company", companyCtrl),
_field("Email", emailCtrl),
_field("Mobile", mobileCtrl),
_field("Address", addressCtrl),
_field("Pincode", pincodeCtrl),
_buildField("Full Name", nameCtrl, scale),
_buildField("Company Name", companyCtrl, scale),
_buildField("Email Address", emailCtrl, scale),
_buildField("Mobile Number", mobileCtrl, scale),
_buildField("Address", addressCtrl, scale),
_buildField("Pincode", pincodeCtrl, scale),
const SizedBox(height: 20),
ElevatedButton(
SizedBox(height: 25 * scale),
/// RESPONSIVE GRADIENT SUBMIT BUTTON
SizedBox(
width: double.infinity,
height: 50 * scale,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14 * scale),
gradient: const LinearGradient(
colors: [
Color(0xFF0052D4),
Color(0xFF4364F7),
Color(0xFF6FB1FC),
],
),
boxShadow: [
BoxShadow(
color: Colors.blueAccent.withOpacity(0.4),
blurRadius: 10 * scale,
offset: Offset(0, 4 * scale),
),
],
),
child: ElevatedButton(
onPressed: _submit,
child: const Text("Submit Update Request"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(14 * scale),
),
),
child: Text(
"Submit Update Request",
style: TextStyle(
fontSize: 17 * scale,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
)
],
),
),
),
SizedBox(height: 30 * scale),
],
),
);
},
),
),
),
);
}
Widget _field(String title, TextEditingController ctrl) {
/// Reusable Responsive TextField Builder
Widget _buildField(
String label, TextEditingController ctrl, double scale) {
return Padding(
padding: const EdgeInsets.only(bottom: 14),
padding: EdgeInsets.only(bottom: 18 * scale),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16 * scale),
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(0.06),
blurRadius: 10 * scale,
offset: Offset(0, 3 * scale),
),
],
),
child: TextField(
controller: ctrl,
style: TextStyle(fontSize: 15 * scale),
decoration: InputDecoration(
labelText: title,
border: OutlineInputBorder(),
filled: true,
fillColor: Colors.white,
labelText: label,
labelStyle: TextStyle(
color: const Color(0xFF003B73),
fontSize: 14 * scale,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16 * scale),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16 * scale),
borderSide: BorderSide(
color: const Color(0xFF003B73),
width: 2 * scale,
),
),
),
),
),
);

View File

@@ -1,6 +1,12 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:share_plus/share_plus.dart';
import '../services/dio_client.dart';
import '../services/invoice_service.dart';
import '../widgets/invoice_detail_view.dart';
class InvoiceDetailScreen extends StatefulWidget {
final int invoiceId;
@@ -20,149 +26,127 @@ class _InvoiceDetailScreenState extends State<InvoiceDetailScreen> {
load();
}
// -------------------------------------------------------
// ⭐ LOAD INVOICE FROM API
// -------------------------------------------------------
Future<void> load() async {
final service = InvoiceService(DioClient.getInstance(context));
try {
final res = await service.getInvoiceDetails(widget.invoiceId);
if (res['success'] == true) {
invoice = res['invoice'] ?? {};
} else {
invoice = {};
}
} catch (e) {
invoice = {};
} finally {
if (mounted) setState(() => loading = false);
}
}
loading = false;
setState(() {});
}
// -------------------------------------------------------
// ⭐ GENERATE + SAVE PDF TO DOWNLOADS FOLDER
// (No Permission Needed)
// -------------------------------------------------------
Future<File> generatePDF() async {
final pdf = pw.Document();
/// ---------- REUSABLE ROW ----------
Widget row(String label, dynamic value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
pdf.addPage(
pw.Page(
build: (context) => pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(fontSize: 14, color: Colors.grey)),
Expanded(
child: Text(
value?.toString().isNotEmpty == true ? value.toString() : "N/A",
textAlign: TextAlign.end,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
pw.Text(
"INVOICE DETAILS",
style: pw.TextStyle(fontSize: 26, fontWeight: pw.FontWeight.bold),
),
pw.SizedBox(height: 20),
pw.Text("Invoice ID: ${invoice['id'] ?? '-'}"),
pw.Text("Amount: ₹${invoice['amount'] ?? '-'}"),
pw.Text("Status: ${invoice['status'] ?? '-'}"),
pw.Text("Date: ${invoice['date'] ?? '-'}"),
pw.Text("Customer: ${invoice['customer_name'] ?? '-'}"),
],
),
),
);
// ⭐ SAFEST WAY (Android 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
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final scale = (width / 430).clamp(0.88, 1.08);
return Scaffold(
appBar: AppBar(title: const Text("Invoice Details")),
body: loading
? const Center(child: CircularProgressIndicator())
/// ================ INVOICE DATA ================
: Padding(
padding: const EdgeInsets.all(16),
child: ListView(
children: [
/// -------- Invoice Summary --------
const Text(
"Invoice Summary",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
appBar: AppBar(
title: Text(
"Invoice Details",
style: TextStyle(
fontSize: 18 * scale,
fontWeight: FontWeight.w600,
),
const SizedBox(height: 10),
row("Invoice No", invoice['invoice_number']),
row("Invoice Date", invoice['invoice_date']),
row("Due Date", invoice['due_date']),
row("Status", invoice['status']),
const Divider(height: 30),
/// -------- Customer Details --------
const Text(
"Customer Details",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
row("Name", invoice['customer_name']),
row("Company", invoice['company_name']),
row("Email", invoice['customer_email']),
row("Mobile", invoice['customer_mobile']),
row("Address", invoice['customer_address']),
row("Pincode", invoice['pincode']),
const Divider(height: 30),
/// -------- Amounts & Taxes --------
const Text(
"Amounts",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
row("Final Amount", invoice['final_amount']),
row("Tax Type", invoice['tax_type']),
row("GST %", invoice['gst_percent']),
row("GST Amount", invoice['gst_amount']),
row("Final with GST", invoice['final_amount_with_gst']),
const Divider(height: 30),
/// -------- Payment Details --------
const Text(
"Payment Details",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
row("Payment Method", invoice['payment_method']),
row("Reference No", invoice['reference_no']),
row("Notes", invoice['notes']),
const Divider(height: 30),
/// -------- PDF --------
if (invoice['pdf_path'] != null)
ElevatedButton.icon(
// ⭐ PDF + SHARE BUTTONS
actions: [
IconButton(
icon: const Icon(Icons.picture_as_pdf),
label: const Text("Download PDF"),
onPressed: () {},
style:
ElevatedButton.styleFrom(backgroundColor: Colors.red),
onPressed: generatePDF,
),
const SizedBox(height: 20),
/// -------- Invoice Items --------
if (invoice['items'] != null)
const Text(
"Invoice Items",
style:
TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
IconButton(
icon: const Icon(Icons.share),
onPressed: sharePDF,
),
const SizedBox(height: 10),
...List.generate(invoice['items']?.length ?? 0, (i) {
final item = invoice['items'][i];
return Card(
child: ListTile(
title: Text(item['description'] ?? "Item"),
subtitle: Text("Qty: ${item['qty'] ?? 0}"),
trailing: Text(
"${item['ttl_amount'] ?? 0}",
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.indigo),
),
),
);
}),
],
),
body: loading
? const Center(child: CircularProgressIndicator())
: invoice.isEmpty
? Center(
child: Text(
"No Invoice Data Found",
style: TextStyle(
fontSize: 18 * scale,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
)
: Padding(
padding: EdgeInsets.all(12 * scale),
child: InvoiceDetailView(invoice: invoice),
),
);
}

View File

@@ -2,11 +2,8 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/invoice_installment_screen.dart';
import '../providers/invoice_provider.dart';
import '../services/dio_client.dart';
import '../services/invoice_service.dart';
import 'invoice_detail_screen.dart';
class InvoiceScreen extends StatefulWidget {
const InvoiceScreen({super.key});
@@ -15,10 +12,11 @@ class InvoiceScreen extends StatefulWidget {
}
class _InvoiceScreenState extends State<InvoiceScreen> {
String searchQuery = "";
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
Provider.of<InvoiceProvider>(context, listen: false)
.loadInvoices(context);
@@ -29,78 +27,263 @@ class _InvoiceScreenState extends State<InvoiceScreen> {
Widget build(BuildContext context) {
final provider = Provider.of<InvoiceProvider>(context);
final width = MediaQuery.of(context).size.width;
final scale = (width / 430).clamp(0.88, 1.08);
if (provider.loading) {
return const Center(child: CircularProgressIndicator());
}
if (provider.invoices.isEmpty) {
return const Center(
child: Text("No invoices found", style: TextStyle(fontSize: 18)));
}
// 🔍 Filter invoices based on search query
final filteredInvoices = provider.invoices.where((inv) {
final q = searchQuery.toLowerCase();
return inv['invoice_number'].toString().toLowerCase().contains(q) ||
inv['invoice_date'].toString().toLowerCase().contains(q) ||
inv['formatted_amount'].toString().toLowerCase().contains(q) ||
inv['status'].toString().toLowerCase().contains(q);
}).toList();
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: provider.invoices.length,
return Column(
children: [
// 🔍 SEARCH BAR
Container(
margin: EdgeInsets.fromLTRB(16 * scale, 16 * scale, 16 * scale, 8 * scale),
padding: EdgeInsets.symmetric(horizontal: 14 * scale),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14 * scale),
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(0.08),
blurRadius: 8 * scale,
offset: Offset(0, 3 * scale),
),
],
),
child: TextField(
onChanged: (v) => setState(() => searchQuery = v),
style: TextStyle(fontSize: 14 * scale),
decoration: InputDecoration(
icon: Icon(Icons.search, size: 22 * scale),
hintText: "Search Invoice Number, Date, Amount...",
border: InputBorder.none,
),
),
),
// 📄 LIST OF INVOICES
Expanded(
child: filteredInvoices.isEmpty
? Center(
child: Text(
"No invoices found",
style: TextStyle(
fontSize: 18 * scale,
fontWeight: FontWeight.w600,
),
),
)
: ListView.builder(
padding: EdgeInsets.all(16 * scale),
itemCount: filteredInvoices.length,
itemBuilder: (_, i) {
final inv = provider.invoices[i];
final inv = filteredInvoices[i];
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.all(14),
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16 * scale),
),
elevation: 3,
margin: EdgeInsets.only(bottom: 14 * scale),
child: Stack(
children: [
Padding(
padding: EdgeInsets.all(16 * scale),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Invoice Number
Text(
"Invoice ${inv['invoice_number'] ?? 'N/A'}",
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold),
style: TextStyle(
fontSize: 20 * scale,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
const SizedBox(height: 6),
Text("Date: ${inv['invoice_date'] ?? 'N/A'}"),
Text("Status: ${inv['status'] ?? 'N/A'}"),
Text("Amount: ₹${inv['formatted_amount'] ?? '0'}"),
SizedBox(height: 8 * scale),
const SizedBox(height: 10),
/// Date + Amount
Text(
"Date: ${inv['invoice_date'] ?? 'N/A'}",
style: TextStyle(fontSize: 15 * scale),
),
Text(
"Amount: ₹${inv['formatted_amount'] ?? '0'}",
style: TextStyle(fontSize: 15 * scale),
),
SizedBox(height: 16 * scale),
/// BUTTONS ROW
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
OutlinedButton(
child: const Text("Invoice Details"),
onPressed: () {
Expanded(
child: GradientButton(
text: "Invoice Details",
fontSize: 15 * scale,
radius: 12 * scale,
padding: 14 * scale,
gradient: const LinearGradient(
colors: [
Color(0xFF1976D2),
Color(0xFF42A5F5),
],
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => InvoiceDetailScreen(
invoiceId: inv['invoice_id'],
),
builder: (_) =>
InvoiceDetailScreen(
invoiceId:
inv['invoice_id']),
),
);
},
),
),
OutlinedButton(
child: const Text("Installments"),
onPressed: () {
SizedBox(width: 12 * scale),
Expanded(
child: GradientButton(
text: "Installments",
fontSize: 15 * scale,
radius: 12 * scale,
padding: 14 * scale,
gradient: const LinearGradient(
colors: [
Color(0xFF43A047),
Color(0xFF81C784),
],
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => InvoiceInstallmentScreen(
invoiceId: inv['invoice_id'],
),
builder: (_) =>
InvoiceInstallmentScreen(
invoiceId:
inv['invoice_id']),
),
);
},
),
),
],
),
],
),
),
/// STATUS BADGE
Positioned(
right: 12 * scale,
top: 12 * scale,
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 10 * scale,
vertical: 6 * scale,
),
decoration: BoxDecoration(
color: _getStatusColor(inv['status']),
borderRadius: BorderRadius.circular(12 * scale),
),
child: Text(
inv['status'] ?? 'N/A',
style: TextStyle(
color: Colors.white,
fontSize: 12 * scale,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
);
},
),
),
],
);
}
}
/// Status Color Helper
Color _getStatusColor(String? status) {
switch (status?.toLowerCase()) {
case 'pending':
return Colors.orange;
case 'in transit':
return Colors.blue;
case 'overdue':
return Colors.redAccent;
case 'paid':
return Colors.green;
default:
return Colors.grey;
}
}
/// -------------------------------------------------------
/// RESPONSIVE GRADIENT BUTTON
/// -------------------------------------------------------
class GradientButton extends StatelessWidget {
final String text;
final Gradient gradient;
final VoidCallback onTap;
final double fontSize;
final double padding;
final double radius;
const GradientButton({
super.key,
required this.text,
required this.gradient,
required this.onTap,
required this.fontSize,
required this.padding,
required this.radius,
});
@override
Widget build(BuildContext context) {
return InkWell(
borderRadius: BorderRadius.circular(radius),
onTap: onTap,
child: Ink(
decoration: BoxDecoration(
gradient: gradient,
borderRadius: BorderRadius.circular(radius),
),
child: Container(
padding: EdgeInsets.symmetric(vertical: padding),
alignment: Alignment.center,
child: Text(
text,
style: TextStyle(
color: Colors.white,
fontSize: fontSize,
fontWeight: FontWeight.w600,
),
),
),
),
);
}
}

View File

@@ -26,27 +26,27 @@ class _LoginScreenState extends State<LoginScreen> {
Future<void> _login() async {
final auth = Provider.of<AuthProvider>(context, listen: false);
final loginId = cLoginId.text.trim();
final password = cPassword.text.trim();
final id = cLoginId.text.trim();
final pass = cPassword.text.trim();
if (loginId.isEmpty || password.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please enter login id and password')),
);
if (id.isEmpty || pass.isEmpty) {
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text("Please fill all fields")));
return;
}
final res = await auth.login(context, loginId, password);
final res = await auth.login(context, id, pass);
if (res['success'] == true) {
if (!mounted) return;
Navigator.of(context).pushReplacement(
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const MainBottomNav()),
);
} else {
final msg = res['message']?.toString() ?? 'Login failed';
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(res['message'] ?? "Login failed")),
);
}
}
@@ -55,38 +55,113 @@ class _LoginScreenState extends State<LoginScreen> {
final auth = Provider.of<AuthProvider>(context);
final width = MediaQuery.of(context).size.width;
/// ⭐ RESPONSIVE SCALE
final scale = (width / 390).clamp(0.85, 1.25);
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
backgroundColor: const Color(0xFFE8F0FF),
body: Stack(
children: [
/// 🔵 Floating Back Button (Responsive Position + Size)
Positioned(
top: 40 * scale,
left: 12 * scale,
child: Material(
elevation: 6 * scale,
color: Colors.indigo.shade700,
shape: const CircleBorder(),
child: InkWell(
onTap: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const WelcomeScreen()),
);
},
child: Padding(
padding: EdgeInsets.all(10 * scale),
child: Icon(Icons.arrow_back,
color: Colors.white, size: 20 * scale),
),
),
),
title: const Text('Login'),
),
body: Padding(
padding: EdgeInsets.symmetric(horizontal: width * 0.06, vertical: 20),
child: Column(
children: [
RoundedInput(
controller: cLoginId,
hint: 'Email / Mobile / Customer ID',
/// 📦 Center White Card (Responsive)
Center(
child: Container(
width: width * 0.87,
padding: EdgeInsets.symmetric(
vertical: 28 * scale,
horizontal: 20 * scale,
),
const SizedBox(height: 12),
RoundedInput(
controller: cPassword,
hint: 'Password',
obscure: true,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(22 * scale),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 18 * scale,
spreadRadius: 1,
offset: Offset(0, 6 * scale),
),
const SizedBox(height: 18),
PrimaryButton(label: 'Login', onTap: _login, busy: auth.loading),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Login",
style: TextStyle(
fontSize: 26 * scale,
fontWeight: FontWeight.bold,
color: Colors.indigo.shade700,
),
),
SizedBox(height: 25 * scale),
/// Login ID Input
Container(
decoration: BoxDecoration(
color: const Color(0xFFD8E7FF),
borderRadius: BorderRadius.circular(14 * scale),
),
child: RoundedInput(
controller: cLoginId,
hint: "Email / Mobile / Customer ID",
),
),
SizedBox(height: 16 * scale),
/// Password Input
Container(
decoration: BoxDecoration(
color: const Color(0xFFD8E7FF),
borderRadius: BorderRadius.circular(14 * scale),
),
child: RoundedInput(
controller: cPassword,
hint: "Password",
obscure: true,
),
),
SizedBox(height: 25 * scale),
/// Login Button
PrimaryButton(
label: "Login",
onTap: _login,
busy: auth.loading,
),
],
),
),
),
],
),
);
}

View File

@@ -17,9 +17,7 @@ class MainBottomNavState extends State<MainBottomNav> {
int _currentIndex = 0;
void setIndex(int index) {
setState(() {
_currentIndex = index;
});
setState(() => _currentIndex = index);
}
final List<Widget> _screens = const [
@@ -30,27 +28,167 @@ class MainBottomNavState extends State<MainBottomNav> {
SettingsScreen(),
];
final List<IconData> _icons = const [
Icons.dashboard_outlined,
Icons.shopping_bag_outlined,
Icons.receipt_long_outlined,
Icons.chat_bubble_outline,
Icons.settings_outlined,
];
final List<String> _labels = const [
"Dashboard",
"Orders",
"Invoice",
"Chat",
"Settings",
];
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final scale = (width / 390).clamp(0.85, 1.20);
final containerPadding = 8 * scale;
return Scaffold(
appBar: const MainAppBar(),
body: _screens[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
selectedItemColor: Colors.red,
unselectedItemColor: Colors.black,
type: BottomNavigationBarType.fixed,
onTap: (index) {
setState(() => _currentIndex = index);
},
items: const [
BottomNavigationBarItem(icon: Icon(Icons.dashboard_outlined), label: "Dashboard"),
BottomNavigationBarItem(icon: Icon(Icons.shopping_bag_outlined), label: "Orders"),
BottomNavigationBarItem(icon: Icon(Icons.receipt_long_outlined), label: "Invoice"),
BottomNavigationBarItem(icon: Icon(Icons.chat_bubble_outline), label: "Chat"),
BottomNavigationBarItem(icon: Icon(Icons.settings_outlined), label: "Settings"),
bottomNavigationBar: Padding(
padding: EdgeInsets.only(
left: 10 * scale,
right: 10 * scale,
bottom: 10 * scale,
),
child: LayoutBuilder(
builder: (context, constraints) {
final totalWidth = constraints.maxWidth;
// inner width (after padding)
final contentWidth = totalWidth - (containerPadding * 2);
final safeContentWidth =
contentWidth > 0 ? contentWidth : totalWidth;
final itemWidth = safeContentWidth / _icons.length;
final indicatorWidth = 70 * scale;
final indicatorHeight = 70 * scale;
double left = (_currentIndex * itemWidth) +
(itemWidth / 2) -
(indicatorWidth / 2);
/// ⭐ FIX: explicitly convert clamp to double
final double safeLeft = left
.clamp(0, safeContentWidth - indicatorWidth)
.toDouble();
return Container(
height: 100 * scale,
padding: EdgeInsets.symmetric(horizontal: containerPadding),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(28 * scale),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 20 * scale,
offset: Offset(0, 8 * scale),
)
],
),
child: Stack(
children: [
/// ⭐ Indicator - safe positioned
AnimatedPositioned(
duration: const Duration(milliseconds: 350),
curve: Curves.easeOut,
top: 10 * scale,
left: safeLeft,
child: Container(
width: indicatorWidth,
height: indicatorHeight,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [
Color(0xFF4F46E5),
Color(0xFF06B6D4),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20 * scale),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
_icons[_currentIndex],
size: 22 * scale,
color: Colors.white,
),
SizedBox(height: 2 * scale),
Text(
_labels[_currentIndex],
style: TextStyle(
fontSize: 10 * scale,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
),
/// ⭐ Icon Row
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: List.generate(_icons.length, (index) {
final selected = index == _currentIndex;
return GestureDetector(
onTap: () => setIndex(index),
child: SizedBox(
width: itemWidth,
height: 100 * scale,
child: Center(
child: AnimatedOpacity(
duration: const Duration(milliseconds: 250),
opacity: selected ? 0 : 1,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
_icons[index],
size: 22 * scale,
color: Colors.black87,
),
SizedBox(height: 4 * scale),
Text(
_labels[index],
style: TextStyle(
fontSize: 10 * scale,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
),
),
),
),
);
}),
),
],
),
);
},
),
),
);
}
}

View File

@@ -10,15 +10,13 @@ class MarkListScreen extends StatefulWidget {
}
class _MarkListScreenState extends State<MarkListScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final provider = Provider.of<MarkListProvider>(context, listen: false);
provider.init(context);
provider.loadMarks(context); // Load full list again
provider.loadMarks(context);
});
}
@@ -26,27 +24,112 @@ class _MarkListScreenState extends State<MarkListScreen> {
Widget build(BuildContext context) {
final marks = Provider.of<MarkListProvider>(context);
// Responsive scale factor
final screenWidth = MediaQuery.of(context).size.width;
final scale = (screenWidth / 390).clamp(0.82, 1.35);
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text("All Mark Numbers"),
backgroundColor: Colors.white,
elevation: 0,
centerTitle: true,
iconTheme: const IconThemeData(color: Colors.black),
title: Text(
"All Mark Numbers",
style: TextStyle(
color: Colors.black,
fontSize: 20 * scale,
fontWeight: FontWeight.w700,
),
),
),
body: marks.loading
? const Center(child: CircularProgressIndicator())
: ListView.builder(
padding: const EdgeInsets.all(12),
padding: EdgeInsets.all(10 * scale), // smaller padding
itemCount: marks.marks.length,
itemBuilder: (_, i) {
final m = marks.marks[i];
return Card(
child: ListTile(
title: Text(m['mark_no']),
subtitle: Text("${m['origin']}${m['destination']}"),
trailing: Text(
m['status'],
style: const TextStyle(color: Colors.indigo),
return Container(
margin: EdgeInsets.only(bottom: 10 * scale), // reduced margin
padding: EdgeInsets.all(12 * scale), // smaller padding
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [
Color(0xFF2196F3),
Color(0xFF64B5F6),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(14 * scale), // smaller radius
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.06),
blurRadius: 5 * scale, // smaller shadow
offset: Offset(0, 2 * scale),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// LEFT TEXT
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// MARK NUMBER
Text(
m['mark_no'],
style: TextStyle(
color: Colors.white,
fontSize: 16 * scale, // reduced font
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 3 * scale),
// ROUTE
Text(
"${m['origin']}${m['destination']}",
style: TextStyle(
color: Colors.white,
fontSize: 13 * scale, // reduced font
fontWeight: FontWeight.w500,
),
),
],
),
),
// STATUS BADGE
Container(
padding: EdgeInsets.symmetric(
horizontal: 10 * scale,
vertical: 5 * scale, // smaller badge
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.92),
borderRadius: BorderRadius.circular(24 * scale),
),
child: Text(
m['status'],
style: TextStyle(
fontSize: 11.5 * scale,
color: Colors.black87,
fontWeight: FontWeight.bold,
),
),
),
],
),
);
},

View File

@@ -1,8 +1,8 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import '../services/dio_client.dart';
import '../services/order_service.dart';
class OrderDetailScreen extends StatefulWidget {
final String orderId;
const OrderDetailScreen({super.key, required this.orderId});
@@ -14,6 +14,7 @@ class OrderDetailScreen extends StatefulWidget {
class _OrderDetailScreenState extends State<OrderDetailScreen> {
bool loading = true;
Map order = {};
final Map<String, bool> _expanded = {};
@override
void initState() {
@@ -33,105 +34,298 @@ class _OrderDetailScreenState extends State<OrderDetailScreen> {
setState(() {});
}
Widget _row(String label, dynamic value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label,
style: const TextStyle(fontSize: 14, color: Colors.grey)),
Text(value?.toString() ?? 'N/A',
style: const TextStyle(
fontSize: 15, fontWeight: FontWeight.w600)),
],
),
);
String _initials(String? s) {
if (s == null || s.isEmpty) return "I";
final parts = s.split(" ");
return parts.take(2).map((e) => e[0].toUpperCase()).join();
}
@override
Widget build(BuildContext context) {
final items = order['items'] ?? [];
final width = MediaQuery.of(context).size.width;
final scale = (width / 430).clamp(0.85, 1.20);
return Scaffold(
appBar: AppBar(title: const Text("Order Details")),
backgroundColor: const Color(0xFFF0F6FF),
appBar: AppBar(
title: const Text("Order Details"),
elevation: 0,
),
body: loading
? const Center(child: CircularProgressIndicator())
: Padding(
padding: const EdgeInsets.all(16),
child: ListView(
children: [
// ---------------- ORDER SUMMARY ----------------
const Text(
"Order Summary",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
_row("Order ID", order['order_id']),
_row("Mark No", order['mark_no']),
_row("Origin", order['origin']),
_row("Destination", order['destination']),
_row("Status", order['status']),
const Divider(height: 30),
const Text(
"Totals",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
_row("CTN", order['ctn']),
_row("Qty", order['qty']),
_row("Total Qty", order['ttl_qty']),
_row("Amount", "${order['ttl_amount'] ?? 0}"),
_row("CBM", order['cbm']),
_row("Total CBM", order['ttl_cbm']),
_row("KG", order['kg']),
_row("Total KG", order['ttl_kg']),
const Divider(height: 30),
// ---------------- ORDER ITEMS ----------------
const Text(
"Order Items",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
...List.generate(items.length, (i) {
final item = items[i];
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.all(12),
padding: EdgeInsets.all(16 * scale),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item['description'] ?? "No description",
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600),
),
const SizedBox(height: 6),
_row("Qty", item['qty']),
_row("Unit", item['unit']),
_row("CBM", item['cbm']),
_row("KG", item['kg']),
_row("Amount", "${item['ttl_amount'] ?? 0}"),
_row("Shop No", item['shop_no']),
_summaryCard(scale),
SizedBox(height: 18 * scale),
_itemsSection(items, scale),
SizedBox(height: 18 * scale),
_totalsSection(scale),
],
),
),
);
}),
],
),
),
);
}
// -----------------------------
// SUMMARY CARD
// -----------------------------
Widget _summaryCard(double scale) {
return Container(
padding: EdgeInsets.all(18 * scale),
decoration: _cardDecoration(scale),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Order Summary",
style: TextStyle(fontSize: 20 * scale, fontWeight: FontWeight.bold)),
SizedBox(height: 12 * scale),
_infoRow("Order ID", order['order_id'], scale),
_infoRow("Mark No", order['mark_no'], scale),
_infoRow("Origin", order['origin'], scale),
_infoRow("Destination", order['destination'], scale),
],
),
);
}
Widget _infoRow(String title, dynamic value, double scale) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 6 * scale),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title,
style: TextStyle(color: Colors.grey, fontSize: 14 * scale)),
Text(value?.toString() ?? "-",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15 * scale)),
],
),
);
}
// -----------------------------
// ORDER ITEMS SECTION
// -----------------------------
Widget _itemsSection(List items, double scale) {
return Container(
padding: EdgeInsets.all(18 * scale),
decoration: _cardDecoration(scale),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Order Items",
style: TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
SizedBox(height: 16 * scale),
...List.generate(items.length, (i) {
return _expandableItem(items[i], i, scale);
})
],
),
);
}
// -----------------------------
// EXPANDABLE ITEM
// -----------------------------
Widget _expandableItem(Map item, int index, double scale) {
final id = "item_$index";
_expanded[id] = _expanded[id] ?? false;
final description = item['description'] ?? "Item";
final initials = _initials(description);
final imageUrl = item['image'] ?? "";
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
margin: EdgeInsets.only(bottom: 16 * scale),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14 * scale),
border: Border.all(color: Colors.black12),
),
child: Column(
children: [
ListTile(
minVerticalPadding: 10 * scale,
leading: _avatar(imageUrl, initials, scale),
title: Text(description,
style:
TextStyle(fontWeight: FontWeight.bold, fontSize: 15 * scale)),
trailing: Transform.rotate(
angle: (_expanded[id]! ? 3.14 : 0),
child: Icon(Icons.keyboard_arrow_down, size: 24 * scale),
),
onTap: () {
setState(() {
_expanded[id] = !_expanded[id]!;
});
},
),
if (_expanded[id]!)
Padding(
padding: EdgeInsets.all(12 * scale),
child: Column(
children: [
_pill("Qty", Icons.list_alt, "${item['qty']}",
Colors.blue.shade100, scale),
_pill("Unit", Icons.category, "${item['unit']}",
Colors.orange.shade100, scale),
_pill("KG", Icons.scale, "${item['kg']}",
Colors.red.shade100, scale),
_pill("CBM", Icons.straighten, "${item['cbm']}",
Colors.purple.shade100, scale),
_pill("Shop", Icons.storefront, "${item['shop_no']}",
Colors.grey.shade300, scale),
_pill("Amount", Icons.currency_rupee,
"${item['ttl_amount']}",
Colors.green.shade100, scale),
],
),
),
],
),
);
}
// -----------------------------
// AVATAR (RESPONSIVE)
// -----------------------------
Widget _avatar(String url, String initials, double scale) {
return Container(
width: 48 * scale,
height: 48 * scale,
decoration: BoxDecoration(
color: Colors.blue.shade200,
borderRadius: BorderRadius.circular(10 * scale),
),
child: url.isNotEmpty
? ClipRRect(
borderRadius: BorderRadius.circular(10 * scale),
child: Image.network(url, fit: BoxFit.cover,
errorBuilder: (c, e, s) => Center(
child: Text(initials,
style: TextStyle(
fontSize: 18 * scale,
color: Colors.white,
fontWeight: FontWeight.bold)),
)),
)
: Center(
child: Text(initials,
style: TextStyle(
fontSize: 18 * scale,
color: Colors.white,
fontWeight: FontWeight.bold)),
),
);
}
// -----------------------------
// COLOR-CODED PILL (RESPONSIVE)
// -----------------------------
Widget _pill(
String title, IconData icon, String value, Color bgColor, double scale) {
return Container(
margin: EdgeInsets.only(bottom: 12 * scale),
padding:
EdgeInsets.symmetric(horizontal: 14 * scale, vertical: 12 * scale),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(12 * scale),
),
child: Row(
children: [
Icon(icon, size: 20 * scale, color: Colors.black54),
SizedBox(width: 10 * scale),
Text("$title: ",
style:
TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * scale)),
Expanded(
child: Text(value,
style: TextStyle(
fontWeight: FontWeight.w700, fontSize: 15 * scale)),
),
],
),
);
}
// -----------------------------
// TOTAL SECTION
// -----------------------------
Widget _totalsSection(double scale) {
return Container(
padding: EdgeInsets.all(18 * scale),
decoration: _cardDecoration(scale),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Totals",
style: TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
SizedBox(height: 12 * scale),
_totalRow("Total Qty", order['ttl_qty'], scale),
_totalRow("Total KG", order['ttl_kg'], scale),
SizedBox(height: 12 * scale),
Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 20 * scale),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(12 * scale),
),
child: Column(
children: [
Text("${order['ttl_amount'] ?? 0}",
style: TextStyle(
color: Colors.green,
fontSize: 28 * scale,
fontWeight: FontWeight.bold,
)),
SizedBox(height: 4 * scale),
Text("Total Amount",
style: TextStyle(
color: Colors.black54, fontSize: 14 * scale)),
],
),
),
],
),
);
}
Widget _totalRow(String title, dynamic value, double scale) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title,
style: TextStyle(color: Colors.grey, fontSize: 14 * scale)),
Text(value?.toString() ?? "0",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15 * scale)),
],
);
}
// -----------------------------
// CARD DECORATION
// -----------------------------
BoxDecoration _cardDecoration(double scale) {
return BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16 * scale),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 8 * scale,
offset: Offset(0, 3 * scale)),
],
);
}
}

View File

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

View File

@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/order_provider.dart';
import 'order_detail_screen.dart';
import 'order_shipment_screen.dart';
import 'order_invoice_screen.dart';
import 'order_track_screen.dart';
@@ -14,6 +13,7 @@ class OrdersScreen extends StatefulWidget {
}
class _OrdersScreenState extends State<OrdersScreen> {
String searchQuery = "";
@override
void initState() {
@@ -31,77 +31,290 @@ class _OrdersScreenState extends State<OrdersScreen> {
return const Center(child: CircularProgressIndicator());
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: provider.orders.length,
itemBuilder: (_, i) {
final o = provider.orders[i];
final screenWidth = MediaQuery.of(context).size.width;
final scale = (screenWidth / 420).clamp(0.85, 1.15);
// FILTER ORDERS
final filteredOrders = provider.orders.where((o) {
final q = searchQuery.toLowerCase();
return o["order_id"].toString().toLowerCase().contains(q) ||
o["status"].toString().toLowerCase().contains(q) ||
o["description"].toString().toLowerCase().contains(q) ||
o["amount"].toString().toLowerCase().contains(q);
}).toList();
return Column(
children: [
// ⭐⭐ WHITE ELEVATED SEARCH BAR ⭐⭐
Container(
margin: EdgeInsets.fromLTRB(16 * scale, 16 * scale, 16 * scale, 10 * scale),
padding: EdgeInsets.symmetric(horizontal: 14 * scale),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16 * scale),
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(0.12),
blurRadius: 10 * scale,
spreadRadius: 1 * scale,
offset: Offset(0, 4 * scale),
),
],
),
child: Row(
children: [
Icon(Icons.search,
size: 22 * scale, color: Colors.grey.shade700),
SizedBox(width: 10 * scale),
Expanded(
child: TextField(
onChanged: (value) => setState(() => searchQuery = value),
style: TextStyle(fontSize: 15 * scale),
decoration: InputDecoration(
hintText: "Search orders...",
hintStyle: TextStyle(
fontSize: 14 * scale,
color: Colors.grey.shade600,
),
border: InputBorder.none,
),
),
),
],
),
),
// LIST OF ORDERS
Expanded(
child: ListView.builder(
padding: EdgeInsets.all(16 * scale),
itemCount: filteredOrders.length,
itemBuilder: (context, i) {
final order = filteredOrders[i];
return _orderCard(order, scale);
},
),
),
],
);
}
// ORDER CARD UI
Widget _orderCard(Map<String, dynamic> o, double scale) {
final progress = getProgress(o['status']);
final badgeColor = getStatusColor(o['status']);
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 3 * scale,
color: Colors.white,
margin: EdgeInsets.only(bottom: 16 * scale),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16 * scale),
),
child: Padding(
padding: const EdgeInsets.all(12),
padding: EdgeInsets.all(16 * scale),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Order ID: ${o['order_id']}",
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text(o['description']),
Text("${o['amount']}"),
Text(o['status'], style: const TextStyle(color: Colors.indigo)),
const SizedBox(height: 10),
// TOP ROW
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_btn("Order", () => _openOrderDetails(o['order_id'])),
_btn("Shipment", () => _openShipment(o['order_id'])),
_btn("Invoice", () => _openInvoice(o['order_id'])),
_btn("Track", () => _openTrack(o['order_id'])),
Text(
"Order #${o['order_id']}",
style: TextStyle(
fontSize: 18 * scale,
fontWeight: FontWeight.bold,
),
),
Container(
padding: EdgeInsets.symmetric(
horizontal: 12 * scale,
vertical: 6 * scale,
),
decoration: BoxDecoration(
color: badgeColor.withOpacity(0.15),
borderRadius: BorderRadius.circular(12 * scale),
),
child: Text(
o['status'],
style: TextStyle(
color: badgeColor,
fontWeight: FontWeight.bold,
fontSize: 13 * scale,
),
),
),
],
),
SizedBox(height: 10 * scale),
Text(
o['description'],
style: TextStyle(fontSize: 14 * scale),
),
SizedBox(height: 5 * scale),
Text(
"${o['amount']}",
style: TextStyle(
fontSize: 16 * scale,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 18 * scale),
_AnimatedProgressBar(progress: progress, scale: scale),
SizedBox(height: 18 * scale),
// BUTTONS
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_btn(Icons.visibility, "View", Colors.green.shade800,
Colors.green.shade50, () => _openOrderDetails(o['order_id']), scale),
_btn(Icons.receipt_long, "Invoice", Colors.orange.shade800,
Colors.orange.shade50, () => _openInvoice(o['order_id']), scale),
_btn(Icons.local_shipping, "Track", Colors.blue.shade800,
Colors.blue.shade50, () => _openTrack(o['order_id']), scale),
],
)
],
),
),
);
},
);
}
Widget _btn(String text, VoidCallback onTap) {
// BUTTON UI
Widget _btn(IconData icon, String text, Color fg, Color bg,
VoidCallback onTap, double scale) {
return InkWell(
borderRadius: BorderRadius.circular(12 * scale),
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
color: Colors.indigo.shade50,
padding: EdgeInsets.symmetric(
horizontal: 20 * scale,
vertical: 12 * scale,
),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(12 * scale),
),
child: Row(
children: [
Icon(icon, size: 18 * scale, color: fg),
SizedBox(width: 8 * scale),
Text(
text,
style: TextStyle(
color: fg,
fontWeight: FontWeight.w600,
fontSize: 14 * scale,
),
),
],
),
child: Text(text, style: const TextStyle(color: Colors.indigo)),
),
);
}
// NAVIGATION
void _openOrderDetails(String id) {
Navigator.push(context, MaterialPageRoute(
builder: (_) => OrderDetailScreen(orderId: id)));
}
void _openShipment(String id) {
Navigator.push(context, MaterialPageRoute(
builder: (_) => OrderShipmentScreen(orderId: id)));
Navigator.push(
context,
MaterialPageRoute(builder: (_) => OrderDetailScreen(orderId: id)),
);
}
void _openInvoice(String id) {
Navigator.push(context, MaterialPageRoute(
builder: (_) => OrderInvoiceScreen(orderId: id)));
Navigator.push(
context,
MaterialPageRoute(builder: (_) => OrderInvoiceScreen(orderId: id)),
);
}
void _openTrack(String id) {
Navigator.push(context, MaterialPageRoute(
builder: (_) => OrderTrackScreen(orderId: id)));
Navigator.push(
context,
MaterialPageRoute(builder: (_) => OrderTrackScreen(orderId: id)),
);
}
}
// PROGRESS BAR
class _AnimatedProgressBar extends StatelessWidget {
final double progress;
final double scale;
const _AnimatedProgressBar({required this.progress, required this.scale});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final maxW = constraints.maxWidth;
return Stack(
children: [
Container(
height: 10 * scale,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(20 * scale),
),
),
AnimatedContainer(
duration: const Duration(milliseconds: 650),
curve: Curves.easeInOut,
height: 10 * scale,
width: maxW * progress,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20 * scale),
gradient: const LinearGradient(
colors: [
Color(0xFF4F8CFF),
Color(0xFF8A4DFF),
],
),
),
),
],
);
},
);
}
}
// PROGRESS VALUES
double getProgress(String? status) {
final s = (status ?? '').toLowerCase();
if (s == "pending") return 0.25;
if (s == "loading") return 0.40;
if (s == "in transit" || s == "intransit") return 0.65;
if (s == "dispatched") return 0.85;
if (s == "delivered") return 1.0;
return 0.05;
}
// STATUS COLORS
Color getStatusColor(String? status) {
final s = (status ?? '').toLowerCase();
if (s == "pending") return Colors.orange;
if (s == "loading") return Colors.amber.shade800;
if (s == "in transit" || s == "intransit") return Colors.red;
if (s == "dispatched") return Colors.blue.shade700;
if (s == "delivered") return Colors.green.shade700;
return Colors.black54;
}

View File

@@ -1,8 +1,9 @@
// lib/screens/order_track_screen.dart
import 'dart:math';
import 'package:flutter/material.dart';
import '../services/dio_client.dart';
import '../services/order_service.dart';
class OrderTrackScreen extends StatefulWidget {
final String orderId;
const OrderTrackScreen({super.key, required this.orderId});
@@ -11,54 +12,579 @@ class OrderTrackScreen extends StatefulWidget {
State<OrderTrackScreen> createState() => _OrderTrackScreenState();
}
class _OrderTrackScreenState extends State<OrderTrackScreen> {
class _OrderTrackScreenState extends State<OrderTrackScreen>
with TickerProviderStateMixin {
bool loading = true;
Map data = {};
Map<String, dynamic>? shipment;
Map<String, dynamic> trackData = {};
late final AnimationController progressController;
late final AnimationController shipController;
@override
void initState() {
super.initState();
load();
}
Future<void> load() async {
final service = OrderService(DioClient.getInstance(context));
final res = await service.trackOrder(widget.orderId);
progressController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 900),
);
if (res['success'] == true) {
data = res['track'];
}
shipController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1600),
)..repeat(reverse: true);
loading = false;
setState(() {});
WidgetsBinding.instance.addPostFrameCallback((_) => _loadData());
}
@override
void dispose() {
progressController.dispose();
shipController.dispose();
super.dispose();
}
// ---------------- LOAD DATA ----------------
Future<void> _loadData() async {
if (!mounted) return;
setState(() => loading = true);
try {
final service = OrderService(DioClient.getInstance(context));
final results = await Future.wait([
service.getShipment(widget.orderId).catchError((_) => {"success": false}),
service.trackOrder(widget.orderId).catchError((_) => {"success": false}),
]);
final shipRes = results[0] as Map;
final trackRes = results[1] as Map;
/// ------------------- SHIPMENT DATA -------------------
shipment = shipRes["success"] == true
? Map<String, dynamic>.from(shipRes["shipment"] ?? {})
: null;
/// ------------------- TRACKING DATA -------------------
trackData = trackRes["success"] == true
? Map<String, dynamic>.from(trackRes["track"] ?? {})
: {};
} catch (_) {}
final target = _computeProgress();
if (mounted) {
try {
await progressController.animateTo(
target,
curve: Curves.easeInOutCubic,
);
} catch (_) {}
}
if (mounted) setState(() => loading = false);
}
// ---------------- PROGRESS LOGIC ----------------
double _computeProgress() {
final status = (trackData["shipment_status"] ?? "")
.toString()
.toLowerCase();
if (status.contains("delivered")) return 1.0;
if (status.contains("dispatched")) return 0.85;
if (status.contains("transit")) return 0.65;
if (status.contains("loading")) return 0.40;
if (status.contains("pending")) return 0.25;
if (_hasTimestamp("delivered_at")) return 1.0;
if (_hasTimestamp("dispatched_at")) return 0.85;
if (_hasTimestamp("in_transit_at")) return 0.65;
if (_hasTimestamp("loading_at")) return 0.40;
if (_hasTimestamp("pending_at")) return 0.25;
return 0.05;
}
bool _hasTimestamp(String key) {
final v = trackData[key];
if (v == null) return false;
final s = v.toString().trim().toLowerCase();
return s.isNotEmpty && s != "null";
}
String _fmt(dynamic v) {
if (v == null) return "-";
try {
final d = DateTime.parse(v.toString()).toLocal();
return "${d.day}/${d.month}/${d.year}";
} catch (_) {
return v.toString();
}
}
// ---------------- UI BUILD ----------------
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final scale = (width / 430).clamp(0.75, 1.25);
return Scaffold(
appBar: AppBar(title: const Text("Track Order")),
appBar: AppBar(
title: const Text("Shipment & Tracking"),
elevation: 0.8,
),
body: loading
? const Center(child: CircularProgressIndicator())
: Padding(
padding: const EdgeInsets.all(16),
: SingleChildScrollView(
padding: EdgeInsets.all(16 * scale),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Order ID: ${data['order_id']}"),
Text("Shipment Status: ${data['shipment_status']}"),
Text("Shipment Date: ${data['shipment_date']}"),
_headerCard(scale),
SizedBox(height: 16 * scale),
const SizedBox(height: 20),
Center(
child: Icon(
Icons.local_shipping,
size: 100,
color: Colors.indigo.shade300,
_shipmentSummary(scale),
SizedBox(height: 16 * scale),
_shipmentTotals(scale),
SizedBox(height: 16 * scale),
_shipmentItems(scale), // USING SHIPMENT API ONLY
SizedBox(height: 20 * scale),
_trackingStatus(scale),
SizedBox(height: 16 * scale),
_progressBar(scale, width),
SizedBox(height: 16 * scale),
_detailsCard(scale),
],
),
),
);
}
// ---------------- HEADER ----------------
Widget _headerCard(double scale) {
return Container(
padding: EdgeInsets.all(14 * scale),
decoration: _boxDecoration(scale),
child: Row(
children: [
Container(
padding: EdgeInsets.all(10 * scale),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(12 * scale),
),
child: Icon(Icons.local_shipping,
color: Colors.blue, size: 28 * scale),
),
SizedBox(width: 12 * scale),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Order #${trackData['order_id'] ?? '-'}",
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 16 * scale),
),
SizedBox(height: 6 * scale),
Text(
trackData["shipment_status"] ?? "-",
style: TextStyle(color: Colors.black54, fontSize: 13 * scale),
),
],
)
],
),
);
}
// ---------------- SHIPMENT SUMMARY ----------------
Widget _shipmentSummary(double scale) {
if (shipment == null) {
return _simpleCard(scale, "Shipment not created yet");
}
return Container(
padding: EdgeInsets.all(16 * scale),
decoration: _boxDecoration(scale),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Shipment Summary",
style:
TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
SizedBox(height: 12 * scale),
// Shipment ID
Container(
padding: EdgeInsets.all(12 * scale),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(12 * scale),
),
child: Row(
children: [
Icon(Icons.qr_code_2, color: Colors.blue, size: 22 * scale),
SizedBox(width: 10 * scale),
Text("Shipment ID: ${shipment!['shipment_id']}",
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 15 * scale)),
],
),
),
SizedBox(height: 14 * scale),
_twoCol("Status", shipment!['status'], scale),
_twoCol("Shipment Date", _fmt(shipment!['shipment_date']), scale),
_twoCol("Origin", shipment!['origin'], scale),
_twoCol("Destination", shipment!['destination'], scale),
],
),
);
}
Widget _simpleCard(double scale, String text) {
return Container(
padding: EdgeInsets.all(14 * scale),
decoration: _boxDecoration(scale),
child: Text(text,
style: TextStyle(fontSize: 15 * scale, color: Colors.grey.shade700)),
);
}
Widget _twoCol(String title, dynamic value, double scale) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 6 * scale),
child: Row(
children: [
Expanded(
child: Text(title,
style: TextStyle(color: Colors.grey, fontSize: 13 * scale)),
),
Expanded(
child: Text(
value?.toString() ?? "-",
textAlign: TextAlign.right,
style:
TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * scale),
),
),
],
),
);
}
// ---------------- SHIPMENT TOTALS ----------------
Widget _shipmentTotals(double scale) {
if (shipment == null) return const SizedBox.shrink();
return Container(
padding: EdgeInsets.all(14 * scale),
decoration: _boxDecoration(scale),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Totals",
style:
TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
SizedBox(height: 10 * scale),
_twoCol("Total CTN", shipment!['total_ctn'], scale),
_twoCol("Total Qty", shipment!['total_qty'], scale),
_twoCol("Total Amount", shipment!['total_amount'], scale),
_twoCol("Total CBM", shipment!['total_cbm'], scale),
_twoCol("Total KG", shipment!['total_kg'], scale),
],
),
);
}
// ---------------- SHIPMENT ITEMS (FROM SHIPMENT API ONLY) ----------------
Widget _shipmentItems(double scale) {
if (shipment == null) return const SizedBox.shrink();
final items = (shipment!['items'] as List?) ?? [];
return Container(
padding: EdgeInsets.all(14 * scale),
decoration: _boxDecoration(scale),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Shipment Items",
style:
TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
SizedBox(height: 14 * scale),
...items.map((item) {
final orderId = item["order_id"]?.toString() ?? "-";
final qty = item["total_ttl_qty"]?.toString() ?? "-";
final cbm = item["total_ttl_cbm"]?.toString() ?? "-";
final kg = item["total_ttl_kg"]?.toString() ?? "-";
final amount = item["total_amount"]?.toString() ?? "-";
return Card(
elevation: 2,
margin: EdgeInsets.only(bottom: 12 * scale),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14 * scale),
),
child: Padding(
padding: EdgeInsets.all(14 * scale),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
Container(
padding: EdgeInsets.all(8 * scale),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(12 * scale),
),
child: Icon(Icons.inventory_2,
color: Colors.blue, size: 20 * scale),
),
SizedBox(width: 12 * scale),
Text(
"Order ID: $orderId",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15 * scale),
),
],
),
SizedBox(height: 12 * scale),
if (item["mark_no"] != null)
Text("Mark No: ${item["mark_no"]}",
style: TextStyle(fontSize: 13 * scale)),
SizedBox(height: 6 * scale),
Text("Quantity: $qty",
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14 * scale)),
Text("CBM: $cbm", style: TextStyle(fontSize: 13 * scale)),
Text("KG: $kg", style: TextStyle(fontSize: 13 * scale)),
SizedBox(height: 10 * scale),
Container(
padding: EdgeInsets.symmetric(
vertical: 8 * scale, horizontal: 12 * scale),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(10 * scale),
border: Border.all(color: Colors.blue, width: 1),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Icon(Icons.currency_rupee,
color: Colors.blue),
Text(
"Amount: ₹$amount",
style: TextStyle(
fontSize: 15 * scale,
fontWeight: FontWeight.bold,
color: Colors.blue),
),
],
),
),
],
),
),
);
}).toList(),
],
),
);
}
// ---------------- TRACKING STATUS ----------------
Widget _trackingStatus(double scale) {
final p = _computeProgress();
final delivered = p >= 1.0;
return Container(
padding:
EdgeInsets.symmetric(vertical: 10 * scale, horizontal: 14 * scale),
decoration: BoxDecoration(
gradient: delivered
? const LinearGradient(colors: [Colors.green, Colors.lightGreen])
: const LinearGradient(colors: [Colors.blue, Colors.purple]),
borderRadius: BorderRadius.circular(40 * scale),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
delivered ? Icons.verified : Icons.local_shipping,
color: Colors.white,
size: 16 * scale,
),
SizedBox(width: 8 * scale),
Text(
(trackData["shipment_status"] ?? "-")
.toString()
.toUpperCase(),
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 13 * scale),
),
],
),
);
}
// ---------------- PROGRESS BAR ----------------
Widget _progressBar(double scale, double width) {
final usableWidth = width - (48 * scale);
return Container(
padding: EdgeInsets.all(12 * scale),
decoration: _boxDecoration(scale),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Shipment Progress",
style:
TextStyle(fontWeight: FontWeight.bold, fontSize: 15 * scale)),
SizedBox(height: 12 * scale),
Stack(
alignment: Alignment.centerLeft,
children: [
// Background
Container(
width: usableWidth,
height: 10 * scale,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(20 * scale)),
),
// Progress Bar
AnimatedBuilder(
animation: progressController,
builder: (_, __) {
final w = usableWidth *
progressController.value.clamp(0.0, 1.0);
return Container(
width: w,
height: 10 * scale,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF4F8CFF), Color(0xFF8A4DFF)]),
borderRadius: BorderRadius.circular(20 * scale),
),
);
},
),
// Moving Truck Icon
AnimatedBuilder(
animation: shipController,
builder: (_, __) {
final progress = progressController.value.clamp(0.0, 1.0);
final bob = sin(shipController.value * 2 * pi) * (4 * scale);
return Positioned(
left: (usableWidth - 26 * scale) * progress,
top: -6 * scale + bob,
child: Container(
width: 26 * scale,
height: 26 * scale,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF4F8CFF), Color(0xFF8A4DFF)],
),
borderRadius: BorderRadius.circular(8 * scale),
),
child: Icon(
Icons.local_shipping,
size: 16 * scale,
color: Colors.white,
),
),
);
},
),
],
),
SizedBox(height: 10 * scale),
AnimatedBuilder(
animation: progressController,
builder: (_, __) => Text(
"${(progressController.value * 100).toInt()}% Completed",
style: TextStyle(color: Colors.black54, fontSize: 13 * scale),
),
)
],
),
);
}
// ---------------- DETAILS CARD ----------------
Widget _detailsCard(double scale) {
return Container(
padding: EdgeInsets.all(14 * scale),
decoration: _boxDecoration(scale),
child: Column(
children: [
_detailsRow("Order ID", trackData["order_id"], scale),
SizedBox(height: 10 * scale),
_detailsRow("Status", trackData["shipment_status"], scale),
SizedBox(height: 10 * scale),
_detailsRow("Date", _fmt(trackData["shipment_date"]), scale),
],
),
);
}
Widget _detailsRow(String title, dynamic value, double scale) {
return Row(
children: [
Expanded(
child: Text(title,
style: TextStyle(
fontWeight: FontWeight.w700, fontSize: 14 * scale))),
Expanded(
child: Text(value?.toString() ?? "-",
textAlign: TextAlign.right,
style: TextStyle(color: Colors.black87, fontSize: 14 * scale))),
],
);
}
// ---------------- BOX DECORATION ----------------
BoxDecoration _boxDecoration(double scale) {
return BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12 * scale),
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(0.05),
blurRadius: 10 * scale,
offset: Offset(0, 4 * scale),
)
],
);
}
}

View File

@@ -16,52 +16,124 @@ class _OtpScreenState extends State<OtpScreen> {
final otpController = TextEditingController();
bool verifying = false;
static const String defaultOtp = '123456'; // default OTP as you said
static const String defaultOtp = '123456';
void _verifyAndSubmit() async {
final entered = otpController.text.trim();
if (entered.length != 6) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Enter 6 digit OTP')));
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('Enter 6 digit OTP')));
return;
}
if (entered != defaultOtp) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Invalid OTP')));
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('Invalid OTP')));
return;
}
setState(() => verifying = true);
// send signup payload to backend
final res = await RequestService(context).sendSignup(widget.signupPayload);
setState(() => verifying = false);
if (res['status'] == true || res['status'] == 'success') {
// navigate to waiting screen
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const WaitingScreen()));
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const WaitingScreen()));
} else {
final message = res['message']?.toString() ?? 'Failed';
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(message)));
}
}
@override
Widget build(BuildContext context) {
final pad = MediaQuery.of(context).size.width * 0.06;
final width = MediaQuery.of(context).size.width;
/// 📌 Universal scale factor for responsiveness
final scale = (width / 390).clamp(0.85, 1.25);
return Scaffold(
appBar: AppBar(title: const Text("OTP Verification")),
body: Padding(
padding: EdgeInsets.symmetric(horizontal: pad, vertical: 20),
child: Column(children: [
const Text("Enter the 6-digit OTP sent to your mobile/email. (Default OTP: 123456)"),
const SizedBox(height: 20),
RoundedInput(controller: otpController, hint: "Enter OTP", keyboardType: TextInputType.number),
const SizedBox(height: 14),
PrimaryButton(label: "Verify & Submit", onTap: _verifyAndSubmit, busy: verifying),
]),
body: SafeArea(
child: Stack(
children: [
/// 🔙 Back Button
Positioned(
top: 18 * scale,
left: 18 * scale,
child: GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
height: 42 * scale,
width: 42 * scale,
decoration: const BoxDecoration(
color: Colors.indigo,
shape: BoxShape.circle,
),
child: Icon(Icons.arrow_back,
color: Colors.white, size: 22 * scale),
),
),
),
/// 🟦 Center Card
Center(
child: Container(
width: width * 0.90,
padding: EdgeInsets.symmetric(
horizontal: 20 * scale, vertical: 28 * scale),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16 * scale),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 12 * scale,
offset: Offset(0, 4 * scale),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"OTP Verification",
style: TextStyle(
fontSize: 22 * scale,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 10 * scale),
Text(
"Enter the 6-digit OTP sent to your mobile/email.\n(Default OTP: 123456)",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 13.5 * scale),
),
SizedBox(height: 18 * scale),
RoundedInput(
controller: otpController,
hint: "Enter OTP",
keyboardType: TextInputType.number,
),
SizedBox(height: 22 * scale),
PrimaryButton(
label: "Verify & Submit",
onTap: _verifyAndSubmit,
busy: verifying,
),
],
),
),
),
],
),
),
);
}
}

View File

@@ -9,13 +9,11 @@ import 'login_screen.dart';
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
@override
void initState() {
super.initState();
@@ -27,33 +25,34 @@ class _SettingsScreenState extends State<SettingsScreen> {
await Future.delayed(const Duration(milliseconds: 100));
}
final profileProvider =
Provider.of<UserProfileProvider>(context, listen: false);
profileProvider.init(context);
await profileProvider.loadProfile(context);
final profile = Provider.of<UserProfileProvider>(context, listen: false);
profile.init(context);
await profile.loadProfile(context);
});
}
Future<void> _pickImage() async {
try {
final picked = await ImagePicker().pickImage(
source: ImageSource.gallery,
imageQuality: 80,
);
if (picked != null) {
final file = File(picked.path);
final profileProvider =
Provider.of<UserProfileProvider>(context, listen: false);
profileProvider.init(context);
final success = await profileProvider.updateProfileImage(context, file);
final profile = Provider.of<UserProfileProvider>(context, listen: false);
profile.init(context);
final ok = await profile.updateProfileImage(context, file);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(success ? "Profile updated" : "Failed to update")),
SnackBar(content: Text(ok ? "Profile updated" : "Failed to update")),
);
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text("Error: $e")));
}
}
Future<void> _logout() async {
@@ -62,131 +61,309 @@ class _SettingsScreenState extends State<SettingsScreen> {
final confirm = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
title: const Text("Logout"),
content: const Text("Are you sure you want to logout?"),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("Cancel")),
TextButton(
child: const Text("Cancel"),
onPressed: () => Navigator.pop(context, false),
),
TextButton(
child: const Text("Logout", style: TextStyle(color: Colors.red)),
onPressed: () => Navigator.pop(context, true),
),
child: const Text("Logout", style: TextStyle(color: Colors.red))),
],
),
);
if (confirm == true) {
await auth.logout(context);
if (!mounted) return;
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => const LoginScreen()),
(route) => false,
(r) => false,
);
}
}
@override
Widget build(BuildContext context) {
final profileProvider = Provider.of<UserProfileProvider>(context);
if (profileProvider.loading || profileProvider.profile == null) {
return const Center(child: CircularProgressIndicator());
// ------------------------- REUSABLE FIELD ROW -------------------------
Widget _fieldRow(IconData icon, String label, String value, double scale) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 12 * scale),
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
Icon(icon, size: 26 * scale, color: Colors.blueGrey.shade700),
SizedBox(width: 14 * scale),
Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(label,
style: TextStyle(
fontSize: 14 * scale,
color: Colors.grey[700],
fontWeight: FontWeight.w700)),
SizedBox(height: 4 * scale),
Text(value,
style: TextStyle(
fontSize: 16 * scale, fontWeight: FontWeight.bold)),
]),
)
]),
);
}
final p = profileProvider.profile!;
return SingleChildScrollView(
padding: const EdgeInsets.all(18),
child: Column(
// ------------------------- INFO TILE -------------------------
Widget _infoTile(IconData icon, String title, String value, double scale) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ------------------ PROFILE IMAGE ------------------
Center(
child: GestureDetector(
onTap: _pickImage,
child: CircleAvatar(
radius: 55,
backgroundImage: p.profileImage != null
? NetworkImage(p.profileImage!)
: null,
child: p.profileImage == null
? const Icon(Icons.person, size: 55)
: null,
),
),
),
const SizedBox(height: 25),
// ------------------ PROFILE INFO ------------------
_infoRow("Customer ID", p.customerId),
_infoRow("Name", p.customerName),
_infoRow("Company", p.companyName),
_infoRow("Email", p.email),
_infoRow("Mobile", p.mobile),
_infoRow("Address", p.address ?? "Not provided"),
_infoRow("Pincode", p.pincode ?? "Not provided"),
_infoRow("Status", p.status ?? "N/A"),
_infoRow("Customer Type", p.customerType ?? "N/A"),
const SizedBox(height: 40),
Center(
child: ElevatedButton(
child: const Text("Edit Profile"),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const EditProfileScreen()),
);
},
),
),
// ------------------ LOGOUT BUTTON ------------------
Center(
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 14),
),
icon: const Icon(Icons.logout, color: Colors.white),
label: const Text(
"Logout",
style: TextStyle(color: Colors.white, fontSize: 16),
),
onPressed: _logout,
),
),
const SizedBox(height: 30),
],
),
);
}
Widget _infoRow(String title, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 14),
Icon(icon, size: 26 * scale, color: Colors.orange.shade800),
SizedBox(width: 14 * scale),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: const TextStyle(
fontSize: 13,
style: TextStyle(
fontSize: 13 * scale,
fontWeight: FontWeight.w600,
color: Colors.grey)),
const SizedBox(height: 4),
Text(value, style: const TextStyle(fontSize: 16)),
color: Colors.black54)),
SizedBox(height: 4 * scale),
Text(value,
style: TextStyle(
fontSize: 17 * scale, fontWeight: FontWeight.bold))
]),
)
],
);
}
@override
Widget build(BuildContext context) {
final profile = Provider.of<UserProfileProvider>(context);
if (profile.loading || profile.profile == null) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
// ----------- RESPONSIVE SCALE -----------
final width = MediaQuery.of(context).size.width;
final scale = (width / 390).clamp(0.80, 1.25);
final p = profile.profile!;
final img = p.profileImage;
final name = p.customerName ?? "Unknown";
final email = p.email ?? "Not provided";
final status = p.status ?? "Active";
final cid = p.customerId ?? "";
final company = p.companyName ?? "";
final type = p.customerType ?? "";
final mobile = p.mobile ?? "";
final address = p.address ?? "Not provided";
final pincode = p.pincode ?? "";
final isPartner = type.toLowerCase().contains("partner");
return Scaffold(
backgroundColor: const Color(0xFFE9F2FF),
body: SafeArea(
child: SingleChildScrollView(
padding: EdgeInsets.all(18 * scale),
child: Column(
children: [
// -------------------- PROFILE SECTION --------------------
Center(
child: Column(
children: [
GestureDetector(
onTap: _pickImage,
child: Stack(
clipBehavior: Clip.none,
children: [
CircleAvatar(
radius: 64 * scale,
backgroundColor: Colors.grey[200],
backgroundImage:
img != null ? NetworkImage(img) : null,
child: img == null
? Icon(Icons.person,
size: 70 * scale, color: Colors.grey[600])
: null,
),
// ------------------ FIXED STATUS BADGE ------------------
Positioned(
bottom: 8 * scale,
right: 8 * scale,
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 12 * scale,
vertical: 6 * scale),
decoration: BoxDecoration(
color: status.toLowerCase() == 'active'
? Colors.green
: Colors.orange,
borderRadius: BorderRadius.circular(20 * scale),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 8 * scale)
],
),
child: Text(
status,
style: TextStyle(
color: Colors.white,
fontSize: 13 * scale,
fontWeight: FontWeight.bold,
),
),
),
)
],
),
),
SizedBox(height: 14 * scale),
Text(name,
style: TextStyle(
fontSize: 20 * scale,
fontWeight: FontWeight.bold)),
SizedBox(height: 6 * scale),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.email,
size: 18 * scale, color: Colors.blueGrey),
SizedBox(width: 8 * scale),
Text(email,
style: TextStyle(
fontSize: 14 * scale,
color: Colors.grey[700])),
],
),
],
),
),
SizedBox(height: 26 * scale),
// ---------------------- YELLOW SUMMARY CARD ----------------------
Container(
padding: EdgeInsets.all(18 * scale),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFFFFF8A3), Color(0xFFFFE275)],
),
borderRadius: BorderRadius.circular(16 * scale),
boxShadow: [
BoxShadow(
color: Colors.orange.withOpacity(0.25),
blurRadius: 14 * scale,
offset: Offset(0, 8 * scale))
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_infoTile(Icons.badge, "Customer ID", cid, scale),
Divider(height: 30 * scale),
_infoTile(Icons.business, "Company Name", company, scale),
Divider(height: 30 * scale),
_infoTile(Icons.category, "Customer Type", type, scale),
SizedBox(height: 20 * scale),
if (isPartner)
Container(
padding: EdgeInsets.all(14 * scale),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14 * scale),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 10 * scale)
],
),
child: Row(children: [
Icon(Icons.workspace_premium,
size: 32 * scale, color: Colors.amber[800]),
SizedBox(width: 12 * scale),
Text("Partner",
style: TextStyle(
fontSize: 16 * scale,
fontWeight: FontWeight.bold)),
]),
),
]),
),
SizedBox(height: 24 * scale),
// ---------------------- DETAILS CARD ----------------------
Container(
padding: EdgeInsets.all(16 * scale),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14 * scale),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.06),
blurRadius: 12 * scale)
]),
child: Column(
children: [
_fieldRow(Icons.phone_android, "Mobile", mobile, scale),
const Divider(),
_fieldRow(Icons.location_on, "Address", address, scale),
const Divider(),
_fieldRow(Icons.local_post_office, "Pincode", pincode, scale),
SizedBox(height: 20 * scale),
Row(children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
const EditProfileScreen()));
},
icon: Icon(Icons.edit,
color: Colors.white, size: 18 * scale),
label: Text("Edit Profile",
style: TextStyle(
color: Colors.white, fontSize: 14 * scale)),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue[700],
padding:
EdgeInsets.symmetric(vertical: 14 * scale),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12 * scale)),
),
)),
SizedBox(width: 12 * scale),
Expanded(
child: ElevatedButton.icon(
onPressed: _logout,
icon: Icon(Icons.logout,
size: 18 * scale, color: Colors.white),
label: Text("Logout",
style: TextStyle(
color: Colors.white, fontSize: 14 * scale)),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[600],
padding:
EdgeInsets.symmetric(vertical: 14 * scale),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12 * scale)),
),
)),
])
],
),
),
SizedBox(height: 30 * scale),
],
),
),
),
);
}

View File

@@ -21,16 +21,20 @@ class _SignupScreenState extends State<SignupScreen> {
bool sending = false;
void _sendOtp() async {
// We don't call backend for OTP here per your flow - OTP is default 123456.
// Validate minimal
if (cName.text.trim().isEmpty || cCompany.text.trim().isEmpty || cEmail.text.trim().isEmpty || cMobile.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please fill the required fields')));
if (cName.text.trim().isEmpty ||
cCompany.text.trim().isEmpty ||
cEmail.text.trim().isEmpty ||
cMobile.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please fill the required fields')),
);
return;
}
setState(() => sending = true);
await Future.delayed(const Duration(milliseconds: 600)); // UI feel
await Future.delayed(const Duration(milliseconds: 600));
setState(() => sending = false);
// Navigate to OTP screen with collected data
final data = {
'customer_name': cName.text.trim(),
'company_name': cCompany.text.trim(),
@@ -40,40 +44,152 @@ class _SignupScreenState extends State<SignupScreen> {
'address': cAddress.text.trim(),
'pincode': cPincode.text.trim(),
};
Navigator.of(context).push(MaterialPageRoute(builder: (_) => OtpScreen(signupPayload: data)));
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => OtpScreen(signupPayload: data),
),
);
}
@override
Widget build(BuildContext context) {
final pad = MediaQuery.of(context).size.width * 0.06;
final width = MediaQuery.of(context).size.width;
return Scaffold(
appBar: AppBar(title: const Text("Create Account")),
backgroundColor: const Color(0xFFE8F0FF), // Same as Login background
body: SafeArea(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: pad),
child: SingleChildScrollView(
child: Column(children: [
const SizedBox(height: 16),
RoundedInput(controller: cName, hint: "Customer name"),
const SizedBox(height: 12),
RoundedInput(controller: cCompany, hint: "Company name"),
const SizedBox(height: 12),
RoundedInput(controller: cDesignation, hint: "Designation (optional)"),
const SizedBox(height: 12),
RoundedInput(controller: cEmail, hint: "Email", keyboardType: TextInputType.emailAddress),
const SizedBox(height: 12),
RoundedInput(controller: cMobile, hint: "Mobile", keyboardType: TextInputType.phone),
const SizedBox(height: 12),
RoundedInput(controller: cAddress, hint: "Address", maxLines: 3),
const SizedBox(height: 12),
RoundedInput(controller: cPincode, hint: "Pincode", keyboardType: TextInputType.number),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// 🔵 Back Button (scrolls with form)
Material(
elevation: 6,
shape: const CircleBorder(),
color: Colors.indigo.shade700,
child: InkWell(
borderRadius: BorderRadius.circular(30),
onTap: () => Navigator.pop(context),
child: const Padding(
padding: EdgeInsets.all(10),
child: Icon(Icons.arrow_back, color: Colors.white),
),
),
),
const SizedBox(height: 20),
PrimaryButton(label: "Send OTP", onTap: _sendOtp, busy: sending),
/// 📦 White Elevated Signup Box
Center(
child: Container(
width: width * 0.88,
padding:
const EdgeInsets.symmetric(vertical: 28, horizontal: 22),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(22),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 18,
spreadRadius: 2,
offset: const Offset(0, 6),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Create Account",
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Colors.indigo.shade700,
),
),
const SizedBox(height: 25),
_blueInput(cName, "Customer name"),
const SizedBox(height: 14),
]),
_blueInput(cCompany, "Company name"),
const SizedBox(height: 14),
_blueInput(cDesignation, "Designation (optional)"),
const SizedBox(height: 14),
_blueInput(
cEmail,
"Email",
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 14),
_blueInput(
cMobile,
"Mobile",
keyboardType: TextInputType.phone,
),
const SizedBox(height: 14),
_blueInput(
cAddress,
"Address",
maxLines: 3,
),
const SizedBox(height: 14),
_blueInput(
cPincode,
"Pincode",
keyboardType: TextInputType.number,
),
const SizedBox(height: 25),
PrimaryButton(
label: "Send OTP",
onTap: _sendOtp,
busy: sending,
),
],
),
),
),
],
),
),
),
),
);
}
/// 🔵 Blue soft background input wrapper (same as Login)
Widget _blueInput(
TextEditingController controller,
String hint, {
TextInputType? keyboardType,
int maxLines = 1,
}) {
return Container(
decoration: BoxDecoration(
color: const Color(0xFFD8E7FF),
borderRadius: BorderRadius.circular(14),
),
child: RoundedInput(
controller: controller,
hint: hint,
// keyboardType: keyboardType,
maxLines: maxLines,
),
);
}
}

View File

@@ -2,7 +2,6 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/auth_provider.dart';
import 'dashboard_screen.dart';
import 'main_bottom_nav.dart';
import 'welcome_screen.dart';
@@ -13,61 +12,166 @@ class SplashScreen extends StatefulWidget {
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
class _SplashScreenState extends State<SplashScreen>
with TickerProviderStateMixin {
late AnimationController _mainController;
late Animation<double> _scaleAnim;
late Animation<double> _fadeAnim;
late AnimationController _floatController;
late Animation<double> _floatAnim;
@override
void initState() {
super.initState();
// MAIN splash animation
_mainController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
);
_scaleAnim = Tween(begin: 0.6, end: 1.0).animate(
CurvedAnimation(parent: _mainController, curve: Curves.easeOutBack),
);
_fadeAnim = Tween(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _mainController, curve: Curves.easeIn),
);
// FLOATING animation (infinite)
_floatController = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat(reverse: true);
_floatAnim = Tween<double>(begin: -10, end: 10).animate(
CurvedAnimation(parent: _floatController, curve: Curves.easeInOut),
);
_mainController.forward();
_init();
}
void _init() async {
await Future.delayed(const Duration(milliseconds: 500));
Future<void> _init() async {
await Future.delayed(const Duration(milliseconds: 700));
final auth = Provider.of<AuthProvider>(context, listen: false);
// 🟢 IMPORTANT → WAIT FOR PREFERENCES TO LOAD
await auth.init();
if (!mounted) return;
if (auth.isLoggedIn) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const MainBottomNav()),
);
} else {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const WelcomeScreen()),
Future.delayed(const Duration(milliseconds: 900), () {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) =>
auth.isLoggedIn ? const MainBottomNav() : const WelcomeScreen(),
),
);
});
}
@override
void dispose() {
_mainController.dispose();
_floatController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final width = MediaQuery.of(context).size.width;
// Responsive scale factor
final scale = (width / 430).clamp(0.9, 1.3);
return Scaffold(
body: Center(
child: Column(mainAxisSize: MainAxisSize.min, children: [
Container(
width: size.width * 0.34,
height: size.width * 0.34,
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.blue.shade50,
Colors.white,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
width: double.infinity,
height: double.infinity,
child: Center(
child: AnimatedBuilder(
animation: _mainController,
builder: (_, __) {
return Opacity(
opacity: _fadeAnim.value,
child: Transform.translate(
offset: Offset(0, _floatAnim.value), // ⭐ Floating animation
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// ⭐ Animated Floating White Circle Logo
Transform.scale(
scale: _scaleAnim.value,
child: AnimatedBuilder(
animation: _floatController,
builder: (_, __) {
return Container(
width: width * 0.50 * scale,
height: width * 0.50 * scale,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).primaryColor.withOpacity(0.14),
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black
.withOpacity(0.08 + (_floatAnim.value.abs() / 200)),
blurRadius: 25 * scale,
spreadRadius: 4 * scale,
offset: Offset(0, 8 * scale),
),
child: Center(
child: Text(
"K",
],
),
child: Padding(
padding: EdgeInsets.all(28 * scale),
child: Image.asset(
"assets/Images/K.png",
fit: BoxFit.contain,
),
),
);
},
),
),
SizedBox(height: 22 * scale),
Text(
"Kent Logistics",
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor),
fontSize: 22 * scale,
fontWeight: FontWeight.w700,
letterSpacing: 1.1,
),
),
SizedBox(height: 6 * scale),
Text(
"Delivering Excellence",
style: TextStyle(
fontSize: 14 * scale,
color: Colors.black54,
),
)
],
),
),
);
},
),
),
const SizedBox(height: 18),
const Text("Kent Logistics",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)),
]),
),
);
}

View File

@@ -1,37 +1,198 @@
import 'dart:math';
import 'package:flutter/material.dart';
class WaitingScreen extends StatelessWidget {
class WaitingScreen extends StatefulWidget {
const WaitingScreen({super.key});
@override
State<WaitingScreen> createState() => _WaitingScreenState();
}
class _WaitingScreenState extends State<WaitingScreen>
with SingleTickerProviderStateMixin {
late final AnimationController _ctrl;
late final Animation<double> _anim;
@override
void initState() {
super.initState();
_ctrl = AnimationController(
vsync: this, duration: const Duration(milliseconds: 1200));
_anim = Tween<double>(begin: 0.0, end: pi).animate(
CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut));
_ctrl.repeat();
}
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
Matrix4 _buildTransform(double value) {
return Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateX(value);
}
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
/// ⭐ Universal scaling factor for responsiveness
final scale = (width / 390).clamp(0.85, 1.3);
return Scaffold(
appBar: AppBar(title: const Text("Request Submitted")),
body: Padding(
padding: const EdgeInsets.all(18.0),
child: Center(
child: Column(mainAxisSize: MainAxisSize.min, children: [
Icon(Icons.hourglass_top, size: 72, color: Theme.of(context).primaryColor),
const SizedBox(height: 16),
const Text(
"Signup request submitted successfully.",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
body: SafeArea(
child: Stack(
children: [
/// BACK BUTTON
Positioned(
top: 12 * scale,
left: 12 * scale,
child: GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
height: 42 * scale,
width: 42 * scale,
decoration: const BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
Color(0xFF0D47A1),
Color(0xFF6A1B9A),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Icon(Icons.arrow_back,
color: Colors.white, size: 20 * scale),
),
),
),
/// MAIN WHITE BOX
Center(
child: Container(
width: width * 0.90,
padding: EdgeInsets.symmetric(
horizontal: 20 * scale, vertical: 30 * scale),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16 * scale),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.10),
blurRadius: 14 * scale,
offset: Offset(0, 4 * scale),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
/// FLIPPING ICON
SizedBox(
height: 92 * scale,
child: AnimatedBuilder(
animation: _anim,
builder: (context, child) {
final angle = _anim.value;
final isBack = angle > (pi / 2);
return Transform(
transform: _buildTransform(angle),
alignment: Alignment.center,
child: Transform(
transform: isBack
? Matrix4.rotationY(pi)
: Matrix4.identity(),
alignment: Alignment.center,
child: child,
),
);
},
child: Icon(
Icons.hourglass_top,
size: 72 * scale,
color: Colors.indigo,
),
),
),
SizedBox(height: 16 * scale),
Text(
"Request Submitted",
style: TextStyle(
fontSize: 22 * scale,
fontWeight: FontWeight.w600),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
const Text(
SizedBox(height: 10 * scale),
Text(
"Signup request submitted successfully.",
style: TextStyle(
fontSize: 18 * scale,
fontWeight: FontWeight.w500),
textAlign: TextAlign.center,
),
SizedBox(height: 8 * scale),
Text(
"Please wait up to 24 hours for admin approval. You will receive an email once approved.",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14 * scale),
),
const SizedBox(height: 24),
ElevatedButton(
SizedBox(height: 24 * scale),
/// BUTTON WITH GRADIENT WRAPPER
Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [
Color(0xFF0D47A1),
Color(0xFF6A1B9A),
],
),
borderRadius: BorderRadius.circular(12 * scale),
),
child: ElevatedButton(
onPressed: () {
Navigator.of(context).popUntil((route) => route.isFirst);
Navigator.of(context)
.popUntil((route) => route.isFirst);
},
child: const Padding(padding: EdgeInsets.symmetric(horizontal: 14, vertical: 12), child: Text("Back to Home")),
style: ElevatedButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: 20 * scale,
vertical: 14 * scale),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(12 * scale),
),
]),
),
child: Text(
"Back to Home",
style: TextStyle(fontSize: 16 * scale),
),
),
),
],
),
),
),
],
),
),
);

View File

@@ -1,39 +1,188 @@
import 'package:flutter/material.dart';
import 'signup_screen.dart';
import 'login_screen.dart';
import '../widgets/primary_button.dart';
class WelcomeScreen extends StatelessWidget {
class WelcomeScreen extends StatefulWidget {
const WelcomeScreen({super.key});
@override
State<WelcomeScreen> createState() => _WelcomeScreenState();
}
class _WelcomeScreenState extends State<WelcomeScreen>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _fade;
late Animation<Offset> _slide;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 900),
);
_fade = CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
);
_slide = Tween<Offset>(
begin: const Offset(0, -0.2),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeInOutBack,
),
);
_controller.forward();
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.repeat(reverse: false);
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Widget _shinyWelcomeText() {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
double shineX = _controller.value % 1;
return Stack(
alignment: Alignment.center,
children: [
Text(
"Welcome",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 42,
fontWeight: FontWeight.bold,
color: Colors.indigo.shade700,
letterSpacing: 1.2,
),
),
Positioned.fill(
child: IgnorePointer(
child: ShaderMask(
blendMode: BlendMode.srcATop,
shaderCallback: (rect) {
final pos = shineX * rect.width;
return LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
stops: [
(pos - 50) / rect.width,
pos / rect.width,
(pos + 50) / rect.width,
],
colors: [
Colors.transparent,
Colors.blueAccent,
Colors.transparent,
],
).createShader(rect);
},
child: Text(
"Welcome",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 42,
fontWeight: FontWeight.bold,
color: Colors.indigo.shade900,
letterSpacing: 1.2,
),
),
),
),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
final w = MediaQuery.of(context).size.width;
final height = MediaQuery.of(context).size.height;
final width = MediaQuery.of(context).size.width;
return Scaffold(
backgroundColor: const Color(0xFFE8F0FF),
body: SafeArea(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: w * 0.06),
padding: EdgeInsets.symmetric(horizontal: width * 0.07),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 28),
Align(alignment: Alignment.centerLeft, child: Text("Welcome", style: Theme.of(context).textTheme.headlineSmall)),
const SizedBox(height: 12),
/// Animated Welcome text
SlideTransition(
position: _slide,
child: _shinyWelcomeText(),
),
SizedBox(height: height * 0.01),
/// LOGO SECTION
Image.asset(
'assets/Images/K.png',
height: height * 0.28,
),
SizedBox(height: height * 0.015),
/// Description Text
const Text(
"Register to access Kent Logistics services. After signup admin will review and approve your request. Approval may take up to 24 hours.",
style: TextStyle(fontSize: 15),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
height: 1.4,
color: Colors.black87,
),
const Spacer(),
ElevatedButton(
onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (_) => const SignupScreen())),
child: const SizedBox(width: double.infinity, child: Center(child: Padding(padding: EdgeInsets.all(14.0), child: Text("Create Account")))),
style: ElevatedButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))),
),
const SizedBox(height: 12),
OutlinedButton(
onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (_) => const LoginScreen())),
child: const SizedBox(width: double.infinity, child: Center(child: Padding(padding: EdgeInsets.all(14.0), child: Text("Login")))),
style: OutlinedButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))),
SizedBox(height: height * 0.04),
/// 🌈 Create Account Button (Gradient)
PrimaryButton(
label: "Create Account",
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const SignupScreen()),
);
},
),
const SizedBox(height: 24),
SizedBox(height: height * 0.015),
/// 🌈 Login Button (Gradient)
PrimaryButton(
label: "Login",
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const LoginScreen()),
);
},
),
SizedBox(height: height * 0.02),
],
),
),

View File

@@ -4,30 +4,55 @@ class InvoiceService {
final Dio dio;
InvoiceService(this.dio);
// -------------------------------------------------------
// ⭐ GET ALL INVOICES
// -------------------------------------------------------
Future<Map<String, dynamic>> getAllInvoices() async {
try {
final res = await dio.get("/user/invoices");
print("🔵 ALL INVOICES RESPONSE:");
print(res.data);
return Map<String, dynamic>.from(res.data);
} catch (e) {
print("❌ ERROR (All Invoices): $e");
return {"success": false, "message": e.toString()};
}
}
// -------------------------------------------------------
// ⭐ GET INSTALLMENTS
// -------------------------------------------------------
Future<Map<String, dynamic>> getInstallments(int invoiceId) async {
try {
final res = await dio.get("/user/invoice/$invoiceId/installments");
final res =
await dio.get("/user/invoice/$invoiceId/installments");
print("🔵 INSTALLMENTS RESPONSE:");
print(res.data);
return Map<String, dynamic>.from(res.data);
} catch (e) {
print("❌ ERROR (Installments): $e");
return {"success": false, "message": e.toString()};
}
}
/// 🔵 NEW FUNCTION — Fetch Full Invoice Details
// -------------------------------------------------------
// ⭐ GET FULL INVOICE DETAILS (PRINT JSON HERE)
// -------------------------------------------------------
Future<Map<String, dynamic>> getInvoiceDetails(int invoiceId) async {
try {
final res = await dio.get("/user/invoice/$invoiceId/details");
print("👇👇👇 INVOICE API RESPONSE START 👇👇👇");
print(res.data); // <-- THIS IS WHAT YOU NEED
print("👆👆👆 INVOICE API RESPONSE END 👆👆👆");
return Map<String, dynamic>.from(res.data);
} catch (e) {
print("❌ ERROR (Invoice Details): $e");
return {"success": false, "message": e.toString()};
}
}

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;
return AppBar(
backgroundColor: Colors.lightGreen,
backgroundColor: Colors.white,
elevation: 0.8,
surfaceTintColor: Colors.transparent,
automaticallyImplyLeading: false,

View File

@@ -1,19 +1,62 @@
import 'package:flutter/material.dart';
class PrimaryButton extends StatelessWidget {
final String label;
final VoidCallback onTap;
final bool busy;
const PrimaryButton({super.key, required this.label, required this.onTap, this.busy = false});
const PrimaryButton({
super.key,
required this.label,
required this.onTap,
this.busy = false,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
child: Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [
Color(0xFF0D47A1), // Blue
Color(0xFF6A1B9A), // Purple
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12),
),
child: ElevatedButton(
onPressed: busy ? null : onTap,
style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))),
child: busy ? const SizedBox(height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) : Text(label, style: const TextStyle(fontSize: 16)),
style: ElevatedButton.styleFrom(
backgroundColor: Colors
.transparent, // IMPORTANT: keep transparent to see gradient
shadowColor: Colors.transparent,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: busy
? const SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Text(
label,
style: const TextStyle(fontSize: 16, color: Colors.white),
),
),
),
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,14 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
archive:
dependency: transitive
description:
name: archive
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
url: "https://pub.dev"
source: hosted
version: "4.0.7"
async:
dependency: transitive
description:
@@ -9,6 +17,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.13.0"
barcode:
dependency: transitive
description:
name: barcode
sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4"
url: "https://pub.dev"
source: hosted
version: "2.2.9"
bidi:
dependency: transitive
description:
name: bidi
sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d"
url: "https://pub.dev"
source: hosted
version: "2.0.13"
boolean_selector:
dependency: transitive
description:
@@ -137,6 +161,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.9.3+5"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
@@ -192,6 +224,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
image:
dependency: transitive
description:
name: image
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
url: "https://pub.dev"
source: hosted
version: "4.5.4"
image_picker:
dependency: "direct main"
description:
@@ -260,26 +300,26 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "10.0.9"
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.9"
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.2"
lints:
dependency: transitive
description:
@@ -308,10 +348,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.16.0"
version: "1.17.0"
mime:
dependency: transitive
description:
@@ -336,8 +376,16 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
path_parsing:
dependency: transitive
description:
name: path_parsing
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
@@ -384,6 +432,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
pdf:
dependency: "direct main"
description:
name: pdf
sha256: "28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416"
url: "https://pub.dev"
source: hosted
version: "3.11.3"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
url: "https://pub.dev"
source: hosted
version: "7.0.1"
platform:
dependency: transitive
description:
@@ -400,6 +464,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
posix:
dependency: transitive
description:
name: posix
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
url: "https://pub.dev"
source: hosted
version: "6.0.3"
provider:
dependency: "direct main"
description:
@@ -408,6 +480,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
qr:
dependency: transitive
description:
name: qr
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
share_plus:
dependency: "direct main"
description:
name: share_plus
sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da
url: "https://pub.dev"
source: hosted
version: "10.1.4"
share_plus_platform_interface:
dependency: transitive
description:
name: share_plus_platform_interface
sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b
url: "https://pub.dev"
source: hosted
version: "5.0.2"
shared_preferences:
dependency: "direct main"
description:
@@ -513,10 +609,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev"
source: hosted
version: "0.7.4"
version: "0.7.7"
typed_data:
dependency: transitive
description:
@@ -525,14 +621,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
url: "https://pub.dev"
source: hosted
version: "3.2.2"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
url: "https://pub.dev"
source: hosted
version: "3.1.5"
uuid:
dependency: transitive
description:
name: uuid
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
url: "https://pub.dev"
source: hosted
version: "4.5.2"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.2.0"
vm_service:
dependency: transitive
description:
@@ -549,6 +685,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
win32:
dependency: transitive
description:
name: win32
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
url: "https://pub.dev"
source: hosted
version: "5.15.0"
xdg_directories:
dependency: transitive
description:
@@ -557,6 +701,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
url: "https://pub.dev"
source: hosted
version: "6.6.1"
sdks:
dart: ">=3.8.1 <4.0.0"
flutter: ">=3.32.0"

View File

@@ -37,6 +37,10 @@ dependencies:
google_fonts: ^4.0.3
image_picker: ^1.0.7
share_plus: ^10.0.0
path_provider: ^2.1.2
pdf: ^3.11.0
# The following adds the Cupertino Icons font to your application.
@@ -64,6 +68,8 @@ flutter:
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
assets:
- assets/Images/K.png
# To add assets to your application, add an assets section, like this:
# assets:

View File

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

View File

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