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

@@ -1,4 +1,3 @@
class ApiConfig {
// Android emulator (use 10.0.2.2), change for physical device or iOS simulator
static const String baseUrl = "http://10.0.2.2:8000/api";
static const String baseUrl = "http://10.207.50.74:8000/api";
}

View File

@@ -0,0 +1,13 @@
class AppConfig {
// For Website & Browser (PC)
static const String logoUrlWeb = "http://127.0.0.1:8000/images/kent_logo2.png";
// For Android Emulator
static const String logoUrlEmulator = "http://10.0.2.2:8000/images/kent_logo2.png";
// For Physical Device (Replace with your actual PC local IP)
static const String logoUrlDevice = "http://10.207.50.74:8000/images/kent_logo2.png";
// Which one to use?
static const String logoUrl = logoUrlDevice; // CHANGE THIS WHEN TESTING ON REAL DEVICE
}

View File

@@ -1,28 +1,78 @@
import 'package:flutter/material.dart';
import 'package:kent_logistics_app/providers/dashboard_provider.dart';
import 'package:kent_logistics_app/providers/invoice_provider.dart';
import 'package:kent_logistics_app/providers/mark_list_provider.dart';
import 'package:kent_logistics_app/providers/order_provider.dart';
import 'package:kent_logistics_app/services/dio_client.dart';
import 'package:kent_logistics_app/services/order_service.dart';
import 'package:provider/provider.dart';
import 'providers/auth_provider.dart';
import 'providers/user_profile_provider.dart'; // NEW IMPORT
import 'screens/splash_screen.dart';
import 'package:google_fonts/google_fonts.dart';
void main() {
runApp(const KentApp());
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final auth = AuthProvider();
await auth.init(); // IMPORTANT: ensure prefs loaded before build
runApp(MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => auth),
ChangeNotifierProvider(create: (_) => UserProfileProvider()),
ChangeNotifierProvider(create: (_) => DashboardProvider()),
ChangeNotifierProvider(create: (_) => MarkListProvider()),
ChangeNotifierProvider(
create: (context) => OrderProvider(
OrderService(DioClient.getInstance(context)),
),
),
ChangeNotifierProvider(create: (_) => InvoiceProvider()),
],
child: const KentApp(),
));
}
class KentApp extends StatelessWidget {
const KentApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => AuthProvider(),
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthProvider()),
ChangeNotifierProvider(create: (_) => UserProfileProvider()), // NEW
ChangeNotifierProvider(create: (_) => DashboardProvider()),
ChangeNotifierProvider(create: (_) => MarkListProvider()),
ChangeNotifierProvider(
create: (context) => OrderProvider(
OrderService(DioClient.getInstance(context)),
),
),
ChangeNotifierProvider(create: (_) => InvoiceProvider()),
],
child: MaterialApp(
title: 'Kent Logistics',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
textTheme: GoogleFonts.interTextTheme(),
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
colorScheme: ColorScheme.fromSeed(seedColor: Colors.red),
scaffoldBackgroundColor: const Color(0xfff8f6ff), // your light background
appBarTheme: const AppBarTheme(
backgroundColor: Colors.indigo, // FIX
foregroundColor: Colors.white, // white text + icons
elevation: 1,
centerTitle: true,
),
),
home: const SplashScreen(),
),
);

View File

@@ -0,0 +1,46 @@
class UserProfile {
final String customerId;
final String customerName;
final String companyName;
final String? designation;
final String email;
final String mobile;
final String? address;
final String? pincode;
final String? status;
final String? customerType;
final String? profileImage;
final String? date;
UserProfile({
required this.customerId,
required this.customerName,
required this.companyName,
this.designation,
required this.email,
required this.mobile,
this.address,
this.pincode,
this.status,
this.customerType,
this.profileImage,
required this.date,
});
factory UserProfile.fromJson(Map<String, dynamic> json) {
return UserProfile(
customerId: json['customer_id'],
customerName: json['customer_name'],
companyName: json['company_name'],
designation: json['designation'],
email: json['email'],
mobile: json['mobile'],
address: json['address'],
pincode: json['pincode'],
status: json['status'],
customerType: json['customer_type'],
profileImage: json['profile_image'],
date: json['date'], // nullable ok now
);
}
}

View File

@@ -4,25 +4,43 @@ import 'package:shared_preferences/shared_preferences.dart';
import '../services/auth_service.dart';
class AuthProvider extends ChangeNotifier {
final AuthService _service = AuthService();
AuthService? _service;
bool _loading = false;
bool get loading => _loading;
bool initialized = false;
String? _token;
Map<String, dynamic>? _user;
String? get token => _token;
Map<String, dynamic>? get user => _user;
bool get isLoggedIn => _token != null && _token!.isNotEmpty;
// Inject context after provider initializes
void initContext(BuildContext context) {
_service = AuthService(context);
}
AuthProvider() {
_loadFromPrefs();
}
// ---------------------- NEW FIX: SAFE INIT -----------------------
Future<void> init() async {
if (!initialized) {
await _loadFromPrefs();
}
}
// ---------------------- LOAD FROM PREFS -----------------------
Future<void> _loadFromPrefs() async {
final prefs = await SharedPreferences.getInstance();
_token = prefs.getString('token');
final userJson = prefs.getString('user');
if (userJson != null) {
try {
@@ -31,22 +49,33 @@ class AuthProvider extends ChangeNotifier {
_user = null;
}
}
initialized = true;
notifyListeners();
}
Future<Map<String, dynamic>> login(String loginId, String password) async {
// -------------------------- LOGIN -----------------------------
Future<Map<String, dynamic>> login(
BuildContext context, String loginId, String password) async {
_service ??= AuthService(context);
_loading = true;
notifyListeners();
final res = await _service.login(loginId, password);
final res = await _service!.login(loginId, password);
_loading = false;
if (res['success'] == true) {
final token = res['token']?.toString();
final userMap = res['user'] is Map ? Map<String, dynamic>.from(res['user']) : null;
final userMap =
res['user'] is Map ? Map<String, dynamic>.from(res['user']) : null;
if (token != null && userMap != null) {
final prefs = await SharedPreferences.getInstance();
await prefs.setString("saved_login_id", loginId);
await prefs.setString("saved_password", password);
await _saveSession(token, userMap);
}
}
@@ -55,27 +84,64 @@ class AuthProvider extends ChangeNotifier {
return res;
}
Future<void> _saveSession(String token, Map<String, dynamic> userMap) async {
// --------------------- SAVE SESSION ---------------------------
Future<void> _saveSession(
String token, Map<String, dynamic> userMap) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('token', token);
await prefs.setString('user', jsonEncode(userMap));
_token = token;
_user = userMap;
notifyListeners();
}
Future<void> logout() async {
// optional: call API logout if implemented
if (_token != null) {
try {
await _service.logout(_token!);
} catch (_) {}
}
// ----------------------- LOGOUT -------------------------------
Future<void> logout(BuildContext context) async {
_service ??= AuthService(context);
try {
await _service!.logout();
} catch (_) {}
final prefs = await SharedPreferences.getInstance();
await prefs.remove('token');
await prefs.remove('user');
_token = null;
_user = null;
notifyListeners();
}
// -------------------- AUTO LOGIN ------------------------------
Future<bool> autoLoginFromSavedCredentials(BuildContext context) async {
final prefs = await SharedPreferences.getInstance();
final loginId = prefs.getString('saved_login_id');
final password = prefs.getString('saved_password');
if (loginId == null || password == null) return false;
final res = await login(context, loginId, password);
return res['success'] == true;
}
// --------------------- REFRESH TOKEN --------------------------
Future<bool> tryRefreshToken(BuildContext context) async {
final prefs = await SharedPreferences.getInstance();
final oldToken = prefs.getString('token');
if (oldToken == null) return false;
_service ??= AuthService(context);
final res = await _service!.refreshToken(oldToken);
if (res['success'] == true && res['token'] != null) {
await prefs.setString('token', res['token']);
_token = res['token'];
notifyListeners();
return true;
}
return false;
}
}

View File

@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import '../services/dashboard_service.dart';
class DashboardProvider extends ChangeNotifier {
DashboardService? _service;
bool loading = false;
int activeOrders = 0;
int inTransitOrders = 0;
int deliveredOrders = 0;
String totalValue = "0";
double totalRaw = 0;
void init(BuildContext context) {
_service = DashboardService(context);
}
Future<void> loadSummary(BuildContext context) async {
loading = true;
notifyListeners();
_service ??= DashboardService(context);
final res = await _service!.getSummary();
if (res['status'] == true) {
final s = res['summary'];
activeOrders = s['active_orders'] ?? 0;
inTransitOrders = s['in_transit_orders'] ?? 0;
deliveredOrders = s['delivered_orders'] ?? 0;
totalValue = s['total_value']?.toString() ?? "0";
totalRaw = double.tryParse(s['total_raw'].toString()) ?? 0;
}
loading = false;
notifyListeners();
}
}

View File

@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import '../services/dio_client.dart';
import '../services/invoice_service.dart';
class InvoiceInstallmentScreen extends StatefulWidget {
final int invoiceId;
const InvoiceInstallmentScreen({super.key, required this.invoiceId});
@override
State<InvoiceInstallmentScreen> createState() =>
_InvoiceInstallmentScreenState();
}
class _InvoiceInstallmentScreenState extends State<InvoiceInstallmentScreen> {
bool loading = true;
List installments = [];
@override
void initState() {
super.initState();
load();
}
Future<void> load() async {
final service = InvoiceService(DioClient.getInstance(context));
final res = await service.getInstallments(widget.invoiceId);
if (res['success'] == true) {
installments = res['installments'] ?? [];
}
loading = false;
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Installments")),
body: loading
? const Center(child: CircularProgressIndicator())
: installments.isEmpty
? const Center(
child: Text("Installments not created yet",
style: TextStyle(fontSize: 18)),
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: installments.length,
itemBuilder: (_, i) {
final inst = installments[i];
return Card(
child: ListTile(
title: Text(
"Amount: ₹${inst['amount']?.toString() ?? '0'}"),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Date: ${inst['installment_date'] ?? 'N/A'}"),
Text(
"Payment: ${inst['payment_method'] ?? 'N/A'}"),
Text(
"Reference: ${inst['reference_no'] ?? 'N/A'}"),
],
),
),
);
},
),
);
}
}

View File

@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import '../services/invoice_service.dart';
import '../services/dio_client.dart';
class InvoiceProvider extends ChangeNotifier {
bool loading = false;
List invoices = [];
Future<void> loadInvoices(BuildContext context) async {
loading = true;
notifyListeners();
final service = InvoiceService(DioClient.getInstance(context));
final res = await service.getAllInvoices();
if (res['success'] == true) {
invoices = res['invoices'] ?? [];
}
loading = false;
notifyListeners();
}
}

View File

@@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
import '../services/mark_list_service.dart';
class MarkListProvider extends ChangeNotifier {
MarkListService? _service;
List<dynamic> marks = [];
bool loading = false;
void init(BuildContext context) {
_service = MarkListService(context);
}
Future<void> loadMarks(BuildContext context) async {
loading = true;
notifyListeners();
_service ??= MarkListService(context);
final res = await _service!.getMarks();
if (res['success'] == true) {
marks = res['data'] ?? [];
}
loading = false;
notifyListeners();
}
Future<Map<String, dynamic>> addMark(BuildContext context, String mark, String origin, String destination) async {
_service ??= MarkListService(context);
final res = await _service!.addMark({
"mark_no": mark,
"origin": origin,
"destination": destination
});
if (res['success'] == true) {
await loadMarks(context); // refresh list
}
return res;
}
}

View File

@@ -0,0 +1,26 @@
import 'package:flutter/cupertino.dart';
import '../services/order_service.dart';
class OrderProvider extends ChangeNotifier {
final OrderService service;
OrderProvider(this.service);
bool loading = false;
List orders = [];
Future<void> loadOrders() async {
loading = true;
notifyListeners();
final res = await service.getOrders();
if (res['success'] == true) {
orders = res['orders'];
}
loading = false;
notifyListeners();
}
}

View File

@@ -0,0 +1,70 @@
import 'dart:io';
import 'package:flutter/material.dart';
import '../models/user_profile.dart';
import '../services/user_profile_service.dart';
class UserProfileProvider extends ChangeNotifier {
UserProfileService? _service;
UserProfile? profile;
bool loading = false;
void init(BuildContext context) {
_service = UserProfileService(context);
}
Future<void> loadProfile(BuildContext context) async {
_service ??= UserProfileService(context);
loading = true;
notifyListeners();
final res = await _service!.getProfile();
if (res['success'] == true) {
profile = UserProfile.fromJson(res['data']);
}
loading = false;
notifyListeners();
}
Future<bool> updateProfileImage(BuildContext context, File image) async {
_service ??= UserProfileService(context);
loading = true;
notifyListeners();
final res = await _service!.updateProfileImage(image);
if (res['success'] == true) {
profile = UserProfile.fromJson(res['data']);
loading = false;
notifyListeners();
return true;
}
loading = false;
notifyListeners();
return false;
}
/// NEW: Send profile update request (admin approval required)
Future<bool> sendProfileUpdateRequest(
BuildContext context,
Map<String, dynamic> data,
) async {
_service ??= UserProfileService(context);
loading = true;
notifyListeners();
final res = await _service!.sendUpdateRequest(data);
loading = false;
notifyListeners();
return res['success'] == true;
}
}

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

View File

@@ -1,16 +1,16 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import '../config/api_config.dart';
import 'dio_client.dart';
class AuthService {
final Dio _dio = Dio(BaseOptions(
baseUrl: ApiConfig.baseUrl,
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 15),
// You can add headers here if needed:
// headers: {'Accept': 'application/json'},
));
late final Dio _dio;
/// Calls /api/user/login with login_id and password
AuthService(BuildContext context) {
_dio = DioClient.getInstance(context);
}
/// Login API
Future<Map<String, dynamic>> login(String loginId, String password) async {
try {
final response = await _dio.post('/user/login', data: {
@@ -18,45 +18,44 @@ class AuthService {
'password': password,
});
// Ensure we return a Map<String, dynamic>
if (response.data is Map) {
return Map<String, dynamic>.from(response.data);
} else {
return {
'success': false,
'message': 'Invalid response from server',
};
}
return Map<String, dynamic>.from(response.data);
} on DioException catch (e) {
// Try to extract message from server response
dynamic respData = e.response?.data;
String message = 'Login failed';
if (respData is Map && respData['message'] != null) {
message = respData['message'].toString();
} else if (e.message != null) {
message = e.message!;
}
final data = e.response?.data;
return {
'success': false,
'message': message,
'message': data is Map && data['message'] != null
? data['message']
: e.message ?? 'Login failed'
};
} catch (e) {
return {
'success': false,
'message': e.toString(),
};
}
}
/// Optional: logout (if you have logout endpoint)
Future<Map<String, dynamic>> logout(String token) async {
try {
final Dio dio = Dio(BaseOptions(baseUrl: ApiConfig.baseUrl));
dio.options.headers['Authorization'] = 'Bearer $token';
final response = await dio.post('/user/logout');
return Map<String, dynamic>.from(response.data ?? {'success': true});
} catch (e) {
return {'success': false, 'message': e.toString()};
}
}
/// Logout API
Future<Map<String, dynamic>> logout() async {
try {
final response = await _dio.post('/user/logout');
return Map<String, dynamic>.from(response.data);
} catch (e) {
return {'success': false, 'message': e.toString()};
}
}
/// Refresh token
Future<Map<String, dynamic>> refreshToken(String oldToken) async {
try {
final response = await _dio.post(
'/user/refresh',
options: Options(headers: {
'Authorization': 'Bearer $oldToken',
}),
);
return Map<String, dynamic>.from(response.data);
} on DioException catch (e) {
final msg = e.response?.data?['message'] ?? 'Refresh failed';
return {'success': false, 'message': msg};
}
}
}

View File

@@ -0,0 +1,20 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'dio_client.dart';
class DashboardService {
late final Dio _dio;
DashboardService(BuildContext context) {
_dio = DioClient.getInstance(context);
}
Future<Map<String, dynamic>> getSummary() async {
try {
final res = await _dio.get('/user/order-summary');
return Map<String, dynamic>.from(res.data);
} catch (e) {
return {'status': false, 'message': e.toString()};
}
}
}

View File

@@ -0,0 +1,33 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../config/api_config.dart';
import '../providers/auth_provider.dart';
import 'token_interceptor.dart';
class DioClient {
static Dio? _dio; // Singleton instance
static Dio getInstance(BuildContext context) {
if (_dio == null) {
_dio = Dio(
BaseOptions(
baseUrl: ApiConfig.baseUrl,
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 15),
headers: {
"Accept": "application/json",
},
),
);
final authProvider = Provider.of<AuthProvider>(context, listen: false);
_dio!.interceptors.add(
TokenInterceptor(authProvider, context, _dio!),
);
}
return _dio!;
}
}

View File

@@ -0,0 +1,34 @@
import 'package:dio/dio.dart';
class InvoiceService {
final Dio dio;
InvoiceService(this.dio);
Future<Map<String, dynamic>> getAllInvoices() async {
try {
final res = await dio.get("/user/invoices");
return Map<String, dynamic>.from(res.data);
} catch (e) {
return {"success": false, "message": e.toString()};
}
}
Future<Map<String, dynamic>> getInstallments(int invoiceId) async {
try {
final res = await dio.get("/user/invoice/$invoiceId/installments");
return Map<String, dynamic>.from(res.data);
} catch (e) {
return {"success": false, "message": e.toString()};
}
}
/// 🔵 NEW FUNCTION — Fetch Full Invoice Details
Future<Map<String, dynamic>> getInvoiceDetails(int invoiceId) async {
try {
final res = await dio.get("/user/invoice/$invoiceId/details");
return Map<String, dynamic>.from(res.data);
} catch (e) {
return {"success": false, "message": e.toString()};
}
}
}

View File

@@ -0,0 +1,29 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'dio_client.dart';
class MarkListService {
late Dio _dio;
MarkListService(BuildContext context) {
_dio = DioClient.getInstance(context);
}
Future<Map<String, dynamic>> addMark(Map<String, dynamic> data) async {
try {
final res = await _dio.post('/add-mark-no', data: data);
return Map<String, dynamic>.from(res.data);
} catch (e) {
return {'success': false, 'message': e.toString()};
}
}
Future<Map<String, dynamic>> getMarks() async {
try {
final res = await _dio.get('/show-mark-list');
return Map<String, dynamic>.from(res.data);
} catch (e) {
return {'success': false, 'message': e.toString()};
}
}
}

View File

@@ -0,0 +1,32 @@
import 'package:dio/dio.dart';
class OrderService {
final Dio _dio;
OrderService(this._dio);
Future<Map<String, dynamic>> getOrders() async {
final res = await _dio.get('/user/orders');
return res.data;
}
Future<Map<String, dynamic>> getOrderDetails(String id) async {
final res = await _dio.get('/user/order/$id/details');
return res.data;
}
Future<Map<String, dynamic>> getShipment(String id) async {
final res = await _dio.get('/user/order/$id/shipment');
return res.data;
}
Future<Map<String, dynamic>> getInvoice(String id) async {
final res = await _dio.get('/user/order/$id/invoice');
return res.data;
}
Future<Map<String, dynamic>> trackOrder(String id) async {
final res = await _dio.get('/user/order/$id/track');
return res.data;
}
}

View File

@@ -1,18 +1,19 @@
import 'package:dio/dio.dart';
import '../config/api_config.dart';
import 'package:flutter/material.dart';
import 'dio_client.dart';
class RequestService {
final Dio _dio = Dio(
BaseOptions(
baseUrl: ApiConfig.baseUrl,
connectTimeout: const Duration(seconds: 15),
),
);
/// Send signup request to backend (after OTP verified)
late final Dio _dio;
RequestService(BuildContext context) {
_dio = DioClient.getInstance(context);
}
/// Signup request after OTP
Future<Map<String, dynamic>> sendSignup(Map<String, dynamic> payload) async {
try {
final resp = await _dio.post('/signup-request', data: payload);
return resp.data is Map ? Map<String, dynamic>.from(resp.data) : {'status': true, 'message': 'OK'};
return Map<String, dynamic>.from(resp.data);
} on DioException catch (e) {
return {
'status': false,

View File

@@ -0,0 +1,37 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import '../providers/auth_provider.dart';
class TokenInterceptor extends Interceptor {
final AuthProvider auth;
final BuildContext context;
final Dio dio;
TokenInterceptor(this.auth, this.context, this.dio);
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
if (auth.token != null) {
options.headers['Authorization'] = 'Bearer ${auth.token}';
}
handler.next(options);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode == 401 &&
err.response?.data['message'] == 'Token has expired') {
final refreshed = await auth.tryRefreshToken(context);
if (refreshed) {
err.requestOptions.headers['Authorization'] = 'Bearer ${auth.token}';
final newResponse = await dio.fetch(err.requestOptions);
return handler.resolve(newResponse);
}
}
handler.next(err);
}
}

View File

@@ -0,0 +1,48 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'dio_client.dart';
class UserProfileService {
late final Dio _dio;
UserProfileService(BuildContext context) {
_dio = DioClient.getInstance(context);
}
/// Get profile
Future<Map<String, dynamic>> getProfile() async {
try {
final res = await _dio.get('/user/profile');
return Map<String, dynamic>.from(res.data);
} catch (e) {
return {"success": false, "message": e.toString()};
}
}
/// Update profile IMAGE only
Future<Map<String, dynamic>> updateProfileImage(File image) async {
try {
final form = FormData.fromMap({
"profile_image": await MultipartFile.fromFile(image.path),
});
final res = await _dio.post('/user/profile-image', data: form);
return Map<String, dynamic>.from(res.data);
} catch (e) {
return {"success": false, "message": e.toString()};
}
}
/// Send profile update request (admin approval needed)
Future<Map<String, dynamic>> sendUpdateRequest(Map<String, dynamic> data) async {
try {
final res = await _dio.post('/user/profile-update-request', data: data);
return Map<String, dynamic>.from(res.data);
} catch (e) {
return {"success": false, "message": e.toString()};
}
}
}

View File

@@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/user_profile_provider.dart';
import '../screens/main_bottom_nav.dart';
import '../config/app_config.dart';
class MainAppBar extends StatelessWidget implements PreferredSizeWidget {
const MainAppBar({super.key});
@override
Widget build(BuildContext context) {
final profileProvider = Provider.of<UserProfileProvider>(context);
final profileUrl = profileProvider.profile?.profileImage;
return AppBar(
backgroundColor: Colors.lightGreen,
elevation: 0.8,
surfaceTintColor: Colors.transparent,
automaticallyImplyLeading: false,
title: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Image.network(
AppConfig.logoUrl,
height: 60,
width: 100,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
const Icon(Icons.business, size: 32, color: Colors.indigo),
),
),
const SizedBox(width: 10),
const Text(
"Kent Logistics",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.indigo,
),
),
],
),
actions: [
IconButton(
icon: const Icon(Icons.notifications_none, color: Colors.indigo),
onPressed: () {},
),
GestureDetector(
onTap: () {
final bottomNav = context.findAncestorStateOfType<MainBottomNavState>();
bottomNav?.setIndex(4);
},
child: Padding(
padding: const EdgeInsets.only(right: 12),
child: CircleAvatar(
radius: 18,
backgroundColor: Colors.grey.shade200,
backgroundImage: profileUrl != null ? NetworkImage(profileUrl) : null,
child: profileUrl == null
? const Icon(Icons.person, color: Colors.grey)
: null,
),
),
),
],
);
}
@override
Size get preferredSize => const Size.fromHeight(56);
}