connect with backend

This commit is contained in:
Abhishek Mali
2025-12-03 11:57:05 +05:30
parent c6b4c66c10
commit 3bf27cc29d
48 changed files with 2618 additions and 126 deletions

View File

@@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
class ChatScreen extends StatelessWidget {
const ChatScreen({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: Text("Invoice Content He", style: TextStyle(fontSize: 18)),
);
}
}

View File

@@ -1,35 +1,260 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/auth_provider.dart';
import 'welcome_screen.dart';
import '../providers/dashboard_provider.dart';
import '../providers/mark_list_provider.dart';
import 'mark_list_screen.dart';
class DashboardScreen extends StatelessWidget {
class DashboardScreen extends StatefulWidget {
const DashboardScreen({super.key});
@override
State<DashboardScreen> createState() => _DashboardScreenState();
}
class _DashboardScreenState extends State<DashboardScreen> {
@override
void initState() {
super.initState();
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);
});
}
void _showAddMarkForm() {
final markCtrl = TextEditingController();
final originCtrl = TextEditingController();
final destCtrl = TextEditingController();
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
builder: (_) {
return Padding(
padding: EdgeInsets.fromLTRB(
18,
18,
18,
MediaQuery.of(context).viewInsets.bottom + 20,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"Add Mark No",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
TextField(controller: markCtrl, decoration: const InputDecoration(labelText: "Mark No")),
const SizedBox(height: 12),
TextField(controller: originCtrl, decoration: const InputDecoration(labelText: "Origin")),
const SizedBox(height: 12),
TextField(controller: destCtrl, decoration: const InputDecoration(labelText: "Destination")),
const SizedBox(height: 20),
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")));
return;
}
final provider = Provider.of<MarkListProvider>(context, listen: false);
final res = await provider.addMark(context, mark, origin, dest);
if (res['success'] == true) {
Navigator.pop(context);
} else {
final msg = res['message'] ?? "Failed";
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
}
},
child: const Text("Submit"),
),
],
),
);
},
);
}
@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';
return Scaffold(
appBar: AppBar(
title: const Text("Dashboard"),
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: () async {
await auth.logout();
Navigator.of(context).pushAndRemoveUntil(MaterialPageRoute(builder: (_) => const WelcomeScreen()), (r) => false);
},
)
if (dash.loading) {
return const Center(child: CircularProgressIndicator());
}
return SingleChildScrollView(
padding: const EdgeInsets.all(18),
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,
children: [
_statBox("Active", dash.activeOrders, Colors.blue),
_statBox("In Transit", dash.inTransitOrders, Colors.orange),
_statBox("Delivered", dash.deliveredOrders, Colors.green),
],
),
const SizedBox(height: 20),
_valueCard("Total Value", dash.totalValue),
const SizedBox(height: 10),
_valueCard("Raw Amount", "${dash.totalRaw.toStringAsFixed(2)}"),
const SizedBox(height: 30),
// 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,
),
if (marks.marks.length > 0)
TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const MarkListScreen()),
);
},
child: const Text(
"View All →",
style: TextStyle(
fontSize: 16,
color: Colors.indigo,
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),
],
),
body: Padding(
padding: const EdgeInsets.all(18),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text("Welcome, ${name}", style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
const SizedBox(height: 12),
const Text("Your dashboard will appear here. Build shipment list, tracking, create shipment screens next."),
]),
);
}
// UI WIDGETS
Widget _statBox(String title, int value, Color color) {
return Container(
width: 110,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Text(value.toString(),
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text(title, style: const TextStyle(fontSize: 14)),
],
),
);
}
Widget _valueCard(String title, String value) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: Colors.indigo.shade50,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: const TextStyle(
fontSize: 14, fontWeight: FontWeight.w600, color: Colors.grey)),
const SizedBox(height: 6),
Text(
value,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.indigo,
),
),
],
),
);
}

View File

@@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/user_profile_provider.dart';
class EditProfileScreen extends StatefulWidget {
const EditProfileScreen({super.key});
@override
State<EditProfileScreen> createState() => _EditProfileScreenState();
}
class _EditProfileScreenState extends State<EditProfileScreen> {
final nameCtrl = TextEditingController();
final companyCtrl = TextEditingController();
final emailCtrl = TextEditingController();
final mobileCtrl = TextEditingController();
final addressCtrl = TextEditingController();
final pincodeCtrl = TextEditingController();
@override
void initState() {
super.initState();
final profile = Provider.of<UserProfileProvider>(
context, listen: false).profile;
nameCtrl.text = profile?.customerName ?? '';
companyCtrl.text = profile?.companyName ?? '';
emailCtrl.text = profile?.email ?? '';
mobileCtrl.text = profile?.mobile ?? '';
addressCtrl.text = profile?.address ?? '';
pincodeCtrl.text = profile?.pincode ?? '';
}
Future<void> _submit() async {
final provider =
Provider.of<UserProfileProvider>(context, listen: false);
final data = {
"customer_name": nameCtrl.text,
"company_name": companyCtrl.text,
"email": emailCtrl.text,
"mobile_no": mobileCtrl.text,
"address": addressCtrl.text,
"pincode": pincodeCtrl.text,
};
final success =
await provider.sendProfileUpdateRequest(context, data);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success
? "Request submitted. Wait for admin approval."
: "Failed to submit request")),
);
if (success) Navigator.pop(context);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Edit Profile")),
body: Padding(
padding: const EdgeInsets.all(18),
child: Column(
children: [
_field("Name", nameCtrl),
_field("Company", companyCtrl),
_field("Email", emailCtrl),
_field("Mobile", mobileCtrl),
_field("Address", addressCtrl),
_field("Pincode", pincodeCtrl),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _submit,
child: const Text("Submit Update Request"),
)
],
),
),
);
}
Widget _field(String title, TextEditingController ctrl) {
return Padding(
padding: const EdgeInsets.only(bottom: 14),
child: TextField(
controller: ctrl,
decoration: InputDecoration(
labelText: title,
border: OutlineInputBorder(),
),
),
);
}
}

View File

@@ -0,0 +1,169 @@
import 'package:flutter/material.dart';
import '../services/dio_client.dart';
import '../services/invoice_service.dart';
class InvoiceDetailScreen extends StatefulWidget {
final int invoiceId;
const InvoiceDetailScreen({super.key, required this.invoiceId});
@override
State<InvoiceDetailScreen> createState() => _InvoiceDetailScreenState();
}
class _InvoiceDetailScreenState extends State<InvoiceDetailScreen> {
bool loading = true;
Map invoice = {};
@override
void initState() {
super.initState();
load();
}
Future<void> load() async {
final service = InvoiceService(DioClient.getInstance(context));
final res = await service.getInvoiceDetails(widget.invoiceId);
if (res['success'] == true) {
invoice = res['invoice'] ?? {};
}
loading = false;
setState(() {});
}
/// ---------- REUSABLE ROW ----------
Widget row(String label, dynamic value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
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,
),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
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),
),
const SizedBox(height: 10),
row("Invoice No", invoice['invoice_number']),
row("Invoice Date", invoice['invoice_date']),
row("Due Date", invoice['due_date']),
row("Status", invoice['status']),
const Divider(height: 30),
/// -------- Customer Details --------
const Text(
"Customer Details",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
row("Name", invoice['customer_name']),
row("Company", invoice['company_name']),
row("Email", invoice['customer_email']),
row("Mobile", invoice['customer_mobile']),
row("Address", invoice['customer_address']),
row("Pincode", invoice['pincode']),
const Divider(height: 30),
/// -------- Amounts & Taxes --------
const Text(
"Amounts",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
row("Final Amount", invoice['final_amount']),
row("Tax Type", invoice['tax_type']),
row("GST %", invoice['gst_percent']),
row("GST Amount", invoice['gst_amount']),
row("Final with GST", invoice['final_amount_with_gst']),
const Divider(height: 30),
/// -------- Payment Details --------
const Text(
"Payment Details",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
row("Payment Method", invoice['payment_method']),
row("Reference No", invoice['reference_no']),
row("Notes", invoice['notes']),
const Divider(height: 30),
/// -------- PDF --------
if (invoice['pdf_path'] != null)
ElevatedButton.icon(
icon: const Icon(Icons.picture_as_pdf),
label: const Text("Download PDF"),
onPressed: () {},
style:
ElevatedButton.styleFrom(backgroundColor: Colors.red),
),
const SizedBox(height: 20),
/// -------- Invoice Items --------
if (invoice['items'] != null)
const Text(
"Invoice Items",
style:
TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
...List.generate(invoice['items']?.length ?? 0, (i) {
final item = invoice['items'][i];
return Card(
child: ListTile(
title: Text(item['description'] ?? "Item"),
subtitle: Text("Qty: ${item['qty'] ?? 0}"),
trailing: Text(
"${item['ttl_amount'] ?? 0}",
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.indigo),
),
),
);
}),
],
),
),
);
}
}

View File

@@ -0,0 +1,106 @@
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});
@override
State<InvoiceScreen> createState() => _InvoiceScreenState();
}
class _InvoiceScreenState extends State<InvoiceScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
Provider.of<InvoiceProvider>(context, listen: false)
.loadInvoices(context);
});
}
@override
Widget build(BuildContext context) {
final provider = Provider.of<InvoiceProvider>(context);
if (provider.loading) {
return const Center(child: CircularProgressIndicator());
}
if (provider.invoices.isEmpty) {
return const Center(
child: Text("No invoices found", style: TextStyle(fontSize: 18)));
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: provider.invoices.length,
itemBuilder: (_, i) {
final inv = provider.invoices[i];
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Invoice ${inv['invoice_number'] ?? 'N/A'}",
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 6),
Text("Date: ${inv['invoice_date'] ?? 'N/A'}"),
Text("Status: ${inv['status'] ?? 'N/A'}"),
Text("Amount: ₹${inv['formatted_amount'] ?? '0'}"),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
OutlinedButton(
child: const Text("Invoice Details"),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => InvoiceDetailScreen(
invoiceId: inv['invoice_id'],
),
),
);
},
),
OutlinedButton(
child: const Text("Installments"),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => InvoiceInstallmentScreen(
invoiceId: inv['invoice_id'],
),
),
);
},
),
],
),
],
),
),
);
},
);
}
}

View File

@@ -1,9 +1,10 @@
import 'package:flutter/material.dart';
import 'package:kent_logistics_app/screens/welcome_screen.dart';
import 'package:provider/provider.dart';
import '../providers/auth_provider.dart';
import '../widgets/rounded_input.dart';
import '../widgets/primary_button.dart';
import 'dashboard_screen.dart';
import 'main_bottom_nav.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@@ -25,21 +26,23 @@ class _LoginScreenState extends State<LoginScreen> {
Future<void> _login() async {
final auth = Provider.of<AuthProvider>(context, listen: false);
// Basic validation
final loginId = cLoginId.text.trim();
final password = cPassword.text.trim();
if (loginId.isEmpty || password.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please enter login id and password')));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please enter login id and password')),
);
return;
}
final res = await auth.login(loginId, password);
final res = await auth.login(context, loginId, password);
// Your controller returns { success: true/false, message, token, user }
if (res['success'] == true) {
// Navigate to dashboard
if (!mounted) return;
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const DashboardScreen()));
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const MainBottomNav()),
);
} else {
final msg = res['message']?.toString() ?? 'Login failed';
if (!mounted) return;
@@ -51,15 +54,35 @@ class _LoginScreenState extends State<LoginScreen> {
Widget build(BuildContext context) {
final auth = Provider.of<AuthProvider>(context);
final width = MediaQuery.of(context).size.width;
return Scaffold(
appBar: AppBar(title: const Text('Login')),
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const WelcomeScreen()),
);
},
),
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', keyboardType: TextInputType.text),
RoundedInput(
controller: cLoginId,
hint: 'Email / Mobile / Customer ID',
),
const SizedBox(height: 12),
RoundedInput(controller: cPassword, hint: 'Password', obscure: true),
RoundedInput(
controller: cPassword,
hint: 'Password',
obscure: true,
),
const SizedBox(height: 18),
PrimaryButton(label: 'Login', onTap: _login, busy: auth.loading),
],

View File

@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import '../widgets/main_app_bar.dart';
import 'dashboard_screen.dart';
import 'order_screen.dart';
import 'invoice_screen.dart';
import 'chat_screen.dart';
import 'settings_screen.dart';
class MainBottomNav extends StatefulWidget {
const MainBottomNav({super.key});
@override
State<MainBottomNav> createState() => MainBottomNavState();
}
class MainBottomNavState extends State<MainBottomNav> {
int _currentIndex = 0;
void setIndex(int index) {
setState(() {
_currentIndex = index;
});
}
final List<Widget> _screens = const [
DashboardScreen(),
OrdersScreen(),
InvoiceScreen(),
ChatScreen(),
SettingsScreen(),
];
@override
Widget build(BuildContext context) {
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"),
],
),
);
}
}

View File

@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/mark_list_provider.dart';
class MarkListScreen extends StatefulWidget {
const MarkListScreen({super.key});
@override
State<MarkListScreen> createState() => _MarkListScreenState();
}
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
});
}
@override
Widget build(BuildContext context) {
final marks = Provider.of<MarkListProvider>(context);
return Scaffold(
appBar: AppBar(
title: const Text("All Mark Numbers"),
),
body: marks.loading
? const Center(child: CircularProgressIndicator())
: ListView.builder(
padding: const EdgeInsets.all(12),
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),
),
),
);
},
),
);
}
}

View File

@@ -0,0 +1,137 @@
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});
@override
State<OrderDetailScreen> createState() => _OrderDetailScreenState();
}
class _OrderDetailScreenState extends State<OrderDetailScreen> {
bool loading = true;
Map order = {};
@override
void initState() {
super.initState();
load();
}
Future<void> load() async {
final service = OrderService(DioClient.getInstance(context));
final res = await service.getOrderDetails(widget.orderId);
if (res['success'] == true) {
order = res['order'];
}
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)),
],
),
);
}
@override
Widget build(BuildContext context) {
final items = order['items'] ?? [];
return Scaffold(
appBar: AppBar(title: const Text("Order Details")),
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),
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']),
],
),
),
);
}),
],
),
),
);
}
}

View File

@@ -0,0 +1,149 @@
import 'package:flutter/material.dart';
import '../services/dio_client.dart';
import '../services/order_service.dart';
class OrderInvoiceScreen extends StatefulWidget {
final String orderId;
const OrderInvoiceScreen({super.key, required this.orderId});
@override
State<OrderInvoiceScreen> createState() => _OrderInvoiceScreenState();
}
class _OrderInvoiceScreenState extends State<OrderInvoiceScreen> {
bool loading = true;
Map invoice = {};
@override
void initState() {
super.initState();
load();
}
Future<void> load() async {
final service = OrderService(DioClient.getInstance(context));
final res = await service.getInvoice(widget.orderId);
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)),
],
),
);
}
@override
Widget build(BuildContext context) {
final items = invoice['items'] ?? [];
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"),
),
const SizedBox(height: 20),
const Text("Invoice Items",
style:
TextStyle(fontSize: 17, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
...List.generate(items.length, (i) {
final item = items[i];
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
title: Text(item['description'] ?? "Item"),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Qty: ${item['qty'] ?? 0}"),
Text("Price: ₹${item['price'] ?? 0}"),
],
),
trailing: Text(
"${item['ttl_amount'] ?? 0}",
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.indigo),
),
),
);
}),
],
),
),
);
}
}

View File

@@ -0,0 +1,107 @@
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';
class OrdersScreen extends StatefulWidget {
const OrdersScreen({super.key});
@override
State<OrdersScreen> createState() => _OrdersScreenState();
}
class _OrdersScreenState extends State<OrdersScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
Provider.of<OrderProvider>(context, listen: false).loadOrders();
});
}
@override
Widget build(BuildContext context) {
final provider = Provider.of<OrderProvider>(context);
if (provider.loading) {
return const Center(child: CircularProgressIndicator());
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: provider.orders.length,
itemBuilder: (_, i) {
final o = provider.orders[i];
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(12),
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),
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'])),
],
)
],
),
),
);
},
);
}
Widget _btn(String text, VoidCallback onTap) {
return InkWell(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
color: Colors.indigo.shade50,
),
child: Text(text, style: const TextStyle(color: Colors.indigo)),
),
);
}
void _openOrderDetails(String id) {
Navigator.push(context, MaterialPageRoute(
builder: (_) => OrderDetailScreen(orderId: id)));
}
void _openShipment(String id) {
Navigator.push(context, MaterialPageRoute(
builder: (_) => OrderShipmentScreen(orderId: id)));
}
void _openInvoice(String id) {
Navigator.push(context, MaterialPageRoute(
builder: (_) => OrderInvoiceScreen(orderId: id)));
}
void _openTrack(String id) {
Navigator.push(context, MaterialPageRoute(
builder: (_) => OrderTrackScreen(orderId: id)));
}
}

View File

@@ -0,0 +1,184 @@
import 'package:flutter/material.dart';
import '../services/dio_client.dart';
import '../services/order_service.dart';
class OrderShipmentScreen extends StatefulWidget {
final String orderId;
const OrderShipmentScreen({super.key, required this.orderId});
@override
State<OrderShipmentScreen> createState() => _OrderShipmentScreenState();
}
class _OrderShipmentScreenState extends State<OrderShipmentScreen> {
bool loading = true;
Map? shipment; // nullable
@override
void initState() {
super.initState();
load();
}
Future<void> load() async {
final service = OrderService(DioClient.getInstance(context));
final res = await service.getShipment(widget.orderId);
if (res['success'] == true) {
shipment = res['shipment']; // may be null
}
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)),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Shipment Details")),
body: loading
? const Center(child: CircularProgressIndicator())
// ---------------------------------------
// 🚨 CASE 1: Shipment NOT created yet
// ---------------------------------------
: (shipment == null)
? const Center(
child: Text(
"Order not shipped yet",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.grey),
),
)
// ---------------------------------------
// 🚛 CASE 2: Shipment available
// ---------------------------------------
: Padding(
padding: const EdgeInsets.all(16),
child: ListView(
children: [
const Text(
"Shipment Summary",
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
_row("Shipment ID", shipment!['shipment_id']),
_row("Status", shipment!['status']),
_row("Shipment Date", shipment!['shipment_date']),
_row("Origin", shipment!['origin']),
_row("Destination", shipment!['destination']),
const Divider(height: 30),
const Text(
"Totals",
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold),
),
_row("Total CTN", shipment!['total_ctn']),
_row("Total Qty", shipment!['total_qty']),
_row("Total TTL Qty", shipment!['total_ttl_qty']),
_row("Total Amount", shipment!['total_amount']),
_row("Total CBM", shipment!['total_cbm']),
_row("Total TTL CBM", shipment!['total_ttl_cbm']),
_row("Total KG", shipment!['total_kg']),
_row("Total TTL KG", shipment!['total_ttl_kg']),
const Divider(height: 30),
_row("Meta", shipment!['meta']),
const Divider(height: 30),
const Text(
"Shipment Items",
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
...List.generate(shipment!['items']?.length ?? 0, (i) {
final item = shipment!['items'][i];
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 🔹 Title: Order ID
Text(
"Order ID: ${item['order_id'] ?? 'N/A'}",
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
// 🔹 Mark No (optional)
if (item['mark_no'] != null)
Text(
"Mark No: ${item['mark_no']}",
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 6),
// 🔹 Total Quantity
Text("Total Qty: ${item['total_ttl_qty'] ?? 0}"),
// 🔹 Total CBM (optional)
if (item['total_ttl_cbm'] != null)
Text("Total CBM: ${item['total_ttl_cbm']}"),
// 🔹 Total KG (optional)
if (item['total_ttl_kg'] != null)
Text("Total KG: ${item['total_ttl_kg']}"),
const SizedBox(height: 6),
// 🔹 Total Amount
Text(
"Amount: ₹${item['total_amount'] ?? 0}",
style: const TextStyle(
color: Colors.indigo,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
})
],
),
),
);
}
}

View File

@@ -0,0 +1,64 @@
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});
@override
State<OrderTrackScreen> createState() => _OrderTrackScreenState();
}
class _OrderTrackScreenState extends State<OrderTrackScreen> {
bool loading = true;
Map data = {};
@override
void initState() {
super.initState();
load();
}
Future<void> load() async {
final service = OrderService(DioClient.getInstance(context));
final res = await service.trackOrder(widget.orderId);
if (res['success'] == true) {
data = res['track'];
}
loading = false;
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Track Order")),
body: loading
? const Center(child: CircularProgressIndicator())
: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Order ID: ${data['order_id']}"),
Text("Shipment Status: ${data['shipment_status']}"),
Text("Shipment Date: ${data['shipment_date']}"),
const SizedBox(height: 20),
Center(
child: Icon(
Icons.local_shipping,
size: 100,
color: Colors.indigo.shade300,
),
),
],
),
),
);
}
}

View File

@@ -33,7 +33,7 @@ class _OtpScreenState extends State<OtpScreen> {
setState(() => verifying = true);
// send signup payload to backend
final res = await RequestService().sendSignup(widget.signupPayload);
final res = await RequestService(context).sendSignup(widget.signupPayload);
setState(() => verifying = false);

View File

@@ -0,0 +1,193 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:provider/provider.dart';
import '../providers/auth_provider.dart';
import '../providers/user_profile_provider.dart';
import 'edit_profile_screen.dart';
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();
WidgetsBinding.instance.addPostFrameCallback((_) async {
final auth = Provider.of<AuthProvider>(context, listen: false);
while (!auth.initialized) {
await Future.delayed(const Duration(milliseconds: 100));
}
final profileProvider =
Provider.of<UserProfileProvider>(context, listen: false);
profileProvider.init(context);
await profileProvider.loadProfile(context);
});
}
Future<void> _pickImage() async {
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);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(success ? "Profile updated" : "Failed to update")),
);
}
}
Future<void> _logout() async {
final auth = Provider.of<AuthProvider>(context, listen: false);
final confirm = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: const Text("Logout"),
content: const Text("Are you sure you want to logout?"),
actions: [
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),
),
],
),
);
if (confirm == true) {
await auth.logout(context);
if (!mounted) return;
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => const LoginScreen()),
(route) => false,
);
}
}
@override
Widget build(BuildContext context) {
final profileProvider = Provider.of<UserProfileProvider>(context);
if (profileProvider.loading || profileProvider.profile == null) {
return const Center(child: CircularProgressIndicator());
}
final p = profileProvider.profile!;
return SingleChildScrollView(
padding: const EdgeInsets.all(18),
child: Column(
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),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.grey)),
const SizedBox(height: 4),
Text(value, style: const TextStyle(fontSize: 16)),
],
),
);
}
}

View File

@@ -3,10 +3,12 @@ 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';
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
@@ -19,15 +21,23 @@ class _SplashScreenState extends State<SplashScreen> {
}
void _init() async {
// small delay to show logo
await Future.delayed(const Duration(milliseconds: 900));
await Future.delayed(const Duration(milliseconds: 500));
final auth = Provider.of<AuthProvider>(context, listen: false);
// ensure provider has loaded prefs
await Future.delayed(const Duration(milliseconds: 300));
// 🟢 IMPORTANT → WAIT FOR PREFERENCES TO LOAD
await auth.init();
if (!mounted) return;
if (auth.isLoggedIn) {
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const DashboardScreen()));
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const MainBottomNav()),
);
} else {
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const WelcomeScreen()));
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const WelcomeScreen()),
);
}
}
@@ -40,11 +50,23 @@ class _SplashScreenState extends State<SplashScreen> {
Container(
width: size.width * 0.34,
height: size.width * 0.34,
decoration: BoxDecoration(shape: BoxShape.circle, color: Theme.of(context).primaryColor.withOpacity(0.14)),
child: Center(child: Text("K", style: TextStyle(fontSize: 48, fontWeight: FontWeight.bold, color: Theme.of(context).primaryColor))),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).primaryColor.withOpacity(0.14),
),
child: Center(
child: Text(
"K",
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor),
),
),
),
const SizedBox(height: 18),
const Text("Kent Logistics", style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)),
const Text("Kent Logistics",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)),
]),
),
);