connect with backend
This commit is contained in:
12
lib/screens/chat_screen.dart
Normal file
12
lib/screens/chat_screen.dart
Normal 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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
98
lib/screens/edit_profile_screen.dart
Normal file
98
lib/screens/edit_profile_screen.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
169
lib/screens/invoice_detail_screen.dart
Normal file
169
lib/screens/invoice_detail_screen.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
106
lib/screens/invoice_screen.dart
Normal file
106
lib/screens/invoice_screen.dart
Normal 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'],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
|
||||
56
lib/screens/main_bottom_nav.dart
Normal file
56
lib/screens/main_bottom_nav.dart
Normal 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"),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
56
lib/screens/mark_list_screen.dart
Normal file
56
lib/screens/mark_list_screen.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
137
lib/screens/order_detail_screen.dart
Normal file
137
lib/screens/order_detail_screen.dart
Normal 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']),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
149
lib/screens/order_invoice_screen.dart
Normal file
149
lib/screens/order_invoice_screen.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
107
lib/screens/order_screen.dart
Normal file
107
lib/screens/order_screen.dart
Normal 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)));
|
||||
}
|
||||
}
|
||||
184
lib/screens/order_shipment_screen.dart
Normal file
184
lib/screens/order_shipment_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
})
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
64
lib/screens/order_track_screen.dart
Normal file
64
lib/screens/order_track_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
193
lib/screens/settings_screen.dart
Normal file
193
lib/screens/settings_screen.dart
Normal 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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user