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

File diff suppressed because one or more lines are too long

View File

@@ -8,7 +8,7 @@ plugins {
android { android {
namespace = "com.example.kent_logistics_app" namespace = "com.example.kent_logistics_app"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion ndkVersion = "27.0.12077973"
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11

View File

@@ -1,8 +1,15 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 🔥 Permissions MUST be here (outside application) -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application <application
android:label="kent_logistics_app" android:label="kent_logistics_app"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@@ -12,34 +19,29 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data <meta-data
android:name="io.flutter.embedding.android.NormalTheme" android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" android:resource="@style/NormalTheme" />
/>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. --> </application>
<queries> <queries>
<intent> <intent>
<action android:name="android.intent.action.PROCESS_TEXT"/> <action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/> <data android:mimeType="text/plain"/>
</intent> </intent>
</queries> </queries>
</manifest> </manifest>

View File

@@ -2,6 +2,11 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>NSPhotoLibraryUsageDescription</key>
<string>App requires photo library access to update profile picture.</string>
<key>NSCameraUsageDescription</key>
<string>App requires camera access to take profile picture.</string>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>

View File

@@ -1,4 +1,3 @@
class ApiConfig { class ApiConfig {
// Android emulator (use 10.0.2.2), change for physical device or iOS simulator static const String baseUrl = "http://10.207.50.74:8000/api";
static const String baseUrl = "http://10.0.2.2: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: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 'package:provider/provider.dart';
import 'providers/auth_provider.dart'; import 'providers/auth_provider.dart';
import 'providers/user_profile_provider.dart'; // NEW IMPORT
import 'screens/splash_screen.dart'; import 'screens/splash_screen.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
void main() { void main() async {
runApp(const KentApp()); 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 { class KentApp extends StatelessWidget {
const KentApp({super.key}); const KentApp({super.key});
@override @override
Widget build(BuildContext context) { 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( child: MaterialApp(
title: 'Kent Logistics', title: 'Kent Logistics',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData( theme: ThemeData(
useMaterial3: true, useMaterial3: true,
textTheme: GoogleFonts.interTextTheme(), 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(), 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'; import '../services/auth_service.dart';
class AuthProvider extends ChangeNotifier { class AuthProvider extends ChangeNotifier {
final AuthService _service = AuthService(); AuthService? _service;
bool _loading = false; bool _loading = false;
bool get loading => _loading; bool get loading => _loading;
bool initialized = false;
String? _token; String? _token;
Map<String, dynamic>? _user; Map<String, dynamic>? _user;
String? get token => _token; String? get token => _token;
Map<String, dynamic>? get user => _user; Map<String, dynamic>? get user => _user;
bool get isLoggedIn => _token != null && _token!.isNotEmpty; bool get isLoggedIn => _token != null && _token!.isNotEmpty;
// Inject context after provider initializes
void initContext(BuildContext context) {
_service = AuthService(context);
}
AuthProvider() { AuthProvider() {
_loadFromPrefs(); _loadFromPrefs();
} }
// ---------------------- NEW FIX: SAFE INIT -----------------------
Future<void> init() async {
if (!initialized) {
await _loadFromPrefs();
}
}
// ---------------------- LOAD FROM PREFS -----------------------
Future<void> _loadFromPrefs() async { Future<void> _loadFromPrefs() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
_token = prefs.getString('token'); _token = prefs.getString('token');
final userJson = prefs.getString('user'); final userJson = prefs.getString('user');
if (userJson != null) { if (userJson != null) {
try { try {
@@ -31,22 +49,33 @@ class AuthProvider extends ChangeNotifier {
_user = null; _user = null;
} }
} }
initialized = true;
notifyListeners(); 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; _loading = true;
notifyListeners(); notifyListeners();
final res = await _service.login(loginId, password); final res = await _service!.login(loginId, password);
_loading = false; _loading = false;
if (res['success'] == true) { if (res['success'] == true) {
final token = res['token']?.toString(); 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) { 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); await _saveSession(token, userMap);
} }
} }
@@ -55,27 +84,64 @@ class AuthProvider extends ChangeNotifier {
return res; 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(); final prefs = await SharedPreferences.getInstance();
await prefs.setString('token', token); await prefs.setString('token', token);
await prefs.setString('user', jsonEncode(userMap)); await prefs.setString('user', jsonEncode(userMap));
_token = token; _token = token;
_user = userMap; _user = userMap;
notifyListeners();
} }
Future<void> logout() async { // ----------------------- LOGOUT -------------------------------
// optional: call API logout if implemented Future<void> logout(BuildContext context) async {
if (_token != null) { _service ??= AuthService(context);
try { try {
await _service.logout(_token!); await _service!.logout();
} catch (_) {} } catch (_) {}
}
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await prefs.remove('token'); await prefs.remove('token');
await prefs.remove('user'); await prefs.remove('user');
_token = null; _token = null;
_user = null; _user = null;
notifyListeners(); 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:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../providers/auth_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}); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final auth = Provider.of<AuthProvider>(context); final auth = Provider.of<AuthProvider>(context);
final dash = Provider.of<DashboardProvider>(context);
final marks = Provider.of<MarkListProvider>(context);
final name = auth.user?['customer_name'] ?? 'User'; final name = auth.user?['customer_name'] ?? 'User';
return Scaffold(
appBar: AppBar( if (dash.loading) {
title: const Text("Dashboard"), return const Center(child: CircularProgressIndicator());
actions: [ }
IconButton(
icon: const Icon(Icons.logout), return SingleChildScrollView(
onPressed: () async { padding: const EdgeInsets.all(18),
await auth.logout(); child: Column(
Navigator.of(context).pushAndRemoveUntil(MaterialPageRoute(builder: (_) => const WelcomeScreen()), (r) => false); 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),
], ],
), ),
body: Padding( 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),
],
),
);
}
// 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), padding: const EdgeInsets.all(18),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ decoration: BoxDecoration(
Text("Welcome, ${name}", style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600)), color: Colors.indigo.shade50,
const SizedBox(height: 12), borderRadius: BorderRadius.circular(12),
const Text("Your dashboard will appear here. Build shipment list, tracking, create shipment screens next."), ),
]), 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:flutter/material.dart';
import 'package:kent_logistics_app/screens/welcome_screen.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../providers/auth_provider.dart'; import '../providers/auth_provider.dart';
import '../widgets/rounded_input.dart'; import '../widgets/rounded_input.dart';
import '../widgets/primary_button.dart'; import '../widgets/primary_button.dart';
import 'dashboard_screen.dart'; import 'main_bottom_nav.dart';
class LoginScreen extends StatefulWidget { class LoginScreen extends StatefulWidget {
const LoginScreen({super.key}); const LoginScreen({super.key});
@@ -25,21 +26,23 @@ class _LoginScreenState extends State<LoginScreen> {
Future<void> _login() async { Future<void> _login() async {
final auth = Provider.of<AuthProvider>(context, listen: false); final auth = Provider.of<AuthProvider>(context, listen: false);
// Basic validation
final loginId = cLoginId.text.trim(); final loginId = cLoginId.text.trim();
final password = cPassword.text.trim(); final password = cPassword.text.trim();
if (loginId.isEmpty || password.isEmpty) { 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; 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) { if (res['success'] == true) {
// Navigate to dashboard
if (!mounted) return; if (!mounted) return;
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const DashboardScreen())); Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const MainBottomNav()),
);
} else { } else {
final msg = res['message']?.toString() ?? 'Login failed'; final msg = res['message']?.toString() ?? 'Login failed';
if (!mounted) return; if (!mounted) return;
@@ -51,15 +54,35 @@ class _LoginScreenState extends State<LoginScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final auth = Provider.of<AuthProvider>(context); final auth = Provider.of<AuthProvider>(context);
final width = MediaQuery.of(context).size.width; final width = MediaQuery.of(context).size.width;
return Scaffold( 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( body: Padding(
padding: EdgeInsets.symmetric(horizontal: width * 0.06, vertical: 20), padding: EdgeInsets.symmetric(horizontal: width * 0.06, vertical: 20),
child: Column( child: Column(
children: [ children: [
RoundedInput(controller: cLoginId, hint: 'Email / Mobile / Customer ID', keyboardType: TextInputType.text), RoundedInput(
controller: cLoginId,
hint: 'Email / Mobile / Customer ID',
),
const SizedBox(height: 12), const SizedBox(height: 12),
RoundedInput(controller: cPassword, hint: 'Password', obscure: true), RoundedInput(
controller: cPassword,
hint: 'Password',
obscure: true,
),
const SizedBox(height: 18), const SizedBox(height: 18),
PrimaryButton(label: 'Login', onTap: _login, busy: auth.loading), 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); setState(() => verifying = true);
// send signup payload to backend // send signup payload to backend
final res = await RequestService().sendSignup(widget.signupPayload); final res = await RequestService(context).sendSignup(widget.signupPayload);
setState(() => verifying = false); 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 'package:provider/provider.dart';
import '../providers/auth_provider.dart'; import '../providers/auth_provider.dart';
import 'dashboard_screen.dart'; import 'dashboard_screen.dart';
import 'main_bottom_nav.dart';
import 'welcome_screen.dart'; import 'welcome_screen.dart';
class SplashScreen extends StatefulWidget { class SplashScreen extends StatefulWidget {
const SplashScreen({super.key}); const SplashScreen({super.key});
@override @override
State<SplashScreen> createState() => _SplashScreenState(); State<SplashScreen> createState() => _SplashScreenState();
} }
@@ -19,15 +21,23 @@ class _SplashScreenState extends State<SplashScreen> {
} }
void _init() async { void _init() async {
// small delay to show logo await Future.delayed(const Duration(milliseconds: 500));
await Future.delayed(const Duration(milliseconds: 900));
final auth = Provider.of<AuthProvider>(context, listen: false); 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) { if (auth.isLoggedIn) {
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const DashboardScreen())); Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const MainBottomNav()),
);
} else { } 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( Container(
width: size.width * 0.34, width: size.width * 0.34,
height: size.width * 0.34, height: size.width * 0.34,
decoration: BoxDecoration(shape: BoxShape.circle, color: Theme.of(context).primaryColor.withOpacity(0.14)), decoration: BoxDecoration(
child: Center(child: Text("K", style: TextStyle(fontSize: 48, fontWeight: FontWeight.bold, color: Theme.of(context).primaryColor))), 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 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:dio/dio.dart';
import 'package:flutter/material.dart';
import '../config/api_config.dart'; import '../config/api_config.dart';
import 'dio_client.dart';
class AuthService { class AuthService {
final Dio _dio = Dio(BaseOptions( late final Dio _dio;
baseUrl: ApiConfig.baseUrl,
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 15),
// You can add headers here if needed:
// headers: {'Accept': 'application/json'},
));
/// 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 { Future<Map<String, dynamic>> login(String loginId, String password) async {
try { try {
final response = await _dio.post('/user/login', data: { final response = await _dio.post('/user/login', data: {
@@ -18,45 +18,44 @@ class AuthService {
'password': password, 'password': password,
}); });
// Ensure we return a Map<String, dynamic>
if (response.data is Map) {
return Map<String, dynamic>.from(response.data); return Map<String, dynamic>.from(response.data);
} else {
return {
'success': false,
'message': 'Invalid response from server',
};
}
} on DioException catch (e) { } on DioException catch (e) {
// Try to extract message from server response final data = e.response?.data;
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!;
}
return { return {
'success': false, '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) { } catch (e) {
return {'success': false, 'message': e.toString()}; 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 'package:dio/dio.dart';
import '../config/api_config.dart'; import 'package:flutter/material.dart';
import 'dio_client.dart';
class RequestService { class RequestService {
final Dio _dio = Dio( late final Dio _dio;
BaseOptions(
baseUrl: ApiConfig.baseUrl, RequestService(BuildContext context) {
connectTimeout: const Duration(seconds: 15), _dio = DioClient.getInstance(context);
), }
);
/// Send signup request to backend (after OTP verified) /// Signup request after OTP
Future<Map<String, dynamic>> sendSignup(Map<String, dynamic> payload) async { Future<Map<String, dynamic>> sendSignup(Map<String, dynamic> payload) async {
try { try {
final resp = await _dio.post('/signup-request', data: payload); 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) { } on DioException catch (e) {
return { return {
'status': false, '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);
}

View File

@@ -6,6 +6,10 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <file_selector_linux/file_selector_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
} }

View File

@@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

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

View File

@@ -41,6 +41,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.19.1" version: "1.19.1"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608"
url: "https://pub.dev"
source: hosted
version: "0.3.5+1"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
@@ -97,6 +105,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.1" version: "7.0.1"
file_selector_linux:
dependency: transitive
description:
name: file_selector_linux
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
url: "https://pub.dev"
source: hosted
version: "0.9.4"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c"
url: "https://pub.dev"
source: hosted
version: "0.9.4+4"
file_selector_platform_interface:
dependency: transitive
description:
name: file_selector_platform_interface
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
url: "https://pub.dev"
source: hosted
version: "2.7.0"
file_selector_windows:
dependency: transitive
description:
name: file_selector_windows
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
url: "https://pub.dev"
source: hosted
version: "0.9.3+5"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -110,6 +150,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.0" version: "5.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: c2fe1001710127dfa7da89977a08d591398370d099aacdaa6d44da7eb14b8476
url: "https://pub.dev"
source: hosted
version: "2.0.31"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@@ -144,6 +192,70 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.2" version: "4.1.2"
image_picker:
dependency: "direct main"
description:
name: image_picker
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e"
url: "https://pub.dev"
source: hosted
version: "0.8.13+1"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e
url: "https://pub.dev"
source: hosted
version: "0.8.13"
image_picker_linux:
dependency: transitive
description:
name: image_picker_linux
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
url: "https://pub.dev"
source: hosted
version: "0.2.2"
image_picker_macos:
dependency: transitive
description:
name: image_picker_macos
sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04
url: "https://pub.dev"
source: hosted
version: "0.2.2"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
url: "https://pub.dev"
source: hosted
version: "2.11.1"
image_picker_windows:
dependency: transitive
description:
name: image_picker_windows
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
url: "https://pub.dev"
source: hosted
version: "0.2.2"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@@ -447,4 +559,4 @@ packages:
version: "1.1.0" version: "1.1.0"
sdks: sdks:
dart: ">=3.8.1 <4.0.0" dart: ">=3.8.1 <4.0.0"
flutter: ">=3.29.0" flutter: ">=3.32.0"

View File

@@ -35,6 +35,8 @@ dependencies:
provider: ^6.0.5 provider: ^6.0.5
shared_preferences: ^2.2.2 shared_preferences: ^2.2.2
google_fonts: ^4.0.3 google_fonts: ^4.0.3
image_picker: ^1.0.7
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.

View File

@@ -6,6 +6,9 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <file_selector_windows/file_selector_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
} }

View File

@@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST