connect with backend
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -8,7 +8,7 @@ plugins {
|
||||
android {
|
||||
namespace = "com.example.kent_logistics_app"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
ndkVersion = "27.0.12077973"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
<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
|
||||
android:label="kent_logistics_app"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@@ -12,34 +19,29 @@
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
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
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
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>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<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>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
13
lib/config/app_config.dart
Normal file
13
lib/config/app_config.dart
Normal 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
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
|
||||
46
lib/models/user_profile.dart
Normal file
46
lib/models/user_profile.dart
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
39
lib/providers/dashboard_provider.dart
Normal file
39
lib/providers/dashboard_provider.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
73
lib/providers/invoice_installment_screen.dart
Normal file
73
lib/providers/invoice_installment_screen.dart
Normal 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'}"),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
23
lib/providers/invoice_provider.dart
Normal file
23
lib/providers/invoice_provider.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
45
lib/providers/mark_list_provider.dart
Normal file
45
lib/providers/mark_list_provider.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
26
lib/providers/order_provider.dart
Normal file
26
lib/providers/order_provider.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
70
lib/providers/user_profile_provider.dart
Normal file
70
lib/providers/user_profile_provider.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
12
lib/screens/chat_screen.dart
Normal file
12
lib/screens/chat_screen.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ChatScreen extends StatelessWidget {
|
||||
const ChatScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(
|
||||
child: Text("Invoice Content He", style: TextStyle(fontSize: 18)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,260 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
import 'welcome_screen.dart';
|
||||
import '../providers/dashboard_provider.dart';
|
||||
import '../providers/mark_list_provider.dart';
|
||||
import 'mark_list_screen.dart';
|
||||
|
||||
class DashboardScreen extends StatelessWidget {
|
||||
class DashboardScreen extends StatefulWidget {
|
||||
const DashboardScreen({super.key});
|
||||
|
||||
@override
|
||||
State<DashboardScreen> createState() => _DashboardScreenState();
|
||||
}
|
||||
|
||||
class _DashboardScreenState extends State<DashboardScreen> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||
|
||||
// STEP 1: Try refresh token BEFORE any API calls
|
||||
await auth.tryRefreshToken(context);
|
||||
|
||||
// STEP 2: Now safe to load dashboard
|
||||
final dash = Provider.of<DashboardProvider>(context, listen: false);
|
||||
dash.init(context);
|
||||
await dash.loadSummary(context);
|
||||
|
||||
// STEP 3: Load marks AFTER refresh
|
||||
final marks = Provider.of<MarkListProvider>(context, listen: false);
|
||||
marks.init(context);
|
||||
await marks.loadMarks(context);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void _showAddMarkForm() {
|
||||
final markCtrl = TextEditingController();
|
||||
final originCtrl = TextEditingController();
|
||||
final destCtrl = TextEditingController();
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
|
||||
builder: (_) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
MediaQuery.of(context).viewInsets.bottom + 20,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
"Add Mark No",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextField(controller: markCtrl, decoration: const InputDecoration(labelText: "Mark No")),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
TextField(controller: originCtrl, decoration: const InputDecoration(labelText: "Origin")),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
TextField(controller: destCtrl, decoration: const InputDecoration(labelText: "Destination")),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final mark = markCtrl.text.trim();
|
||||
final origin = originCtrl.text.trim();
|
||||
final dest = destCtrl.text.trim();
|
||||
|
||||
if (mark.isEmpty || origin.isEmpty || dest.isEmpty) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(content: Text("All fields required")));
|
||||
return;
|
||||
}
|
||||
|
||||
final provider = Provider.of<MarkListProvider>(context, listen: false);
|
||||
final res = await provider.addMark(context, mark, origin, dest);
|
||||
|
||||
if (res['success'] == true) {
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
final msg = res['message'] ?? "Failed";
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||
}
|
||||
},
|
||||
child: const Text("Submit"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final auth = Provider.of<AuthProvider>(context);
|
||||
final dash = Provider.of<DashboardProvider>(context);
|
||||
final marks = Provider.of<MarkListProvider>(context);
|
||||
|
||||
final name = auth.user?['customer_name'] ?? 'User';
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Dashboard"),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
onPressed: () async {
|
||||
await auth.logout();
|
||||
Navigator.of(context).pushAndRemoveUntil(MaterialPageRoute(builder: (_) => const WelcomeScreen()), (r) => false);
|
||||
},
|
||||
)
|
||||
|
||||
if (dash.loading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(18),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// HEADER
|
||||
Text(
|
||||
"Welcome, $name 👋",
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ORDER SUMMARY
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_statBox("Active", dash.activeOrders, Colors.blue),
|
||||
_statBox("In Transit", dash.inTransitOrders, Colors.orange),
|
||||
_statBox("Delivered", dash.deliveredOrders, Colors.green),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_valueCard("Total Value", dash.totalValue),
|
||||
const SizedBox(height: 10),
|
||||
_valueCard("Raw Amount", "₹${dash.totalRaw.toStringAsFixed(2)}"),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// ADD + VIEW ALL BUTTONS SIDE BY SIDE
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text("Add Mark No"),
|
||||
onPressed: _showAddMarkForm,
|
||||
),
|
||||
|
||||
if (marks.marks.length > 0)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const MarkListScreen()),
|
||||
);
|
||||
},
|
||||
child: const Text(
|
||||
"View All →",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.indigo,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// MARK LIST (only 10 latest)
|
||||
const Text(
|
||||
"Latest Mark Numbers",
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
|
||||
if (marks.loading)
|
||||
const Center(child: CircularProgressIndicator())
|
||||
else
|
||||
Column(
|
||||
children: List.generate(
|
||||
marks.marks.length > 10 ? 10 : marks.marks.length,
|
||||
(i) {
|
||||
final m = marks.marks[i];
|
||||
return Card(
|
||||
child: ListTile(
|
||||
title: Text(m['mark_no']),
|
||||
subtitle: Text("${m['origin']} → ${m['destination']}"),
|
||||
trailing: Text(
|
||||
m['status'],
|
||||
style: const TextStyle(color: Colors.indigo),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(18),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text("Welcome, ${name}", style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 12),
|
||||
const Text("Your dashboard will appear here. Build shipment list, tracking, create shipment screens next."),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
// UI WIDGETS
|
||||
Widget _statBox(String title, int value, Color color) {
|
||||
return Container(
|
||||
width: 110,
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(value.toString(),
|
||||
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
Text(title, style: const TextStyle(fontSize: 14)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _valueCard(String title, String value) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.indigo.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14, fontWeight: FontWeight.w600, color: Colors.grey)),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.indigo,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
98
lib/screens/edit_profile_screen.dart
Normal file
98
lib/screens/edit_profile_screen.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/user_profile_provider.dart';
|
||||
|
||||
class EditProfileScreen extends StatefulWidget {
|
||||
const EditProfileScreen({super.key});
|
||||
|
||||
@override
|
||||
State<EditProfileScreen> createState() => _EditProfileScreenState();
|
||||
}
|
||||
|
||||
class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||
final nameCtrl = TextEditingController();
|
||||
final companyCtrl = TextEditingController();
|
||||
final emailCtrl = TextEditingController();
|
||||
final mobileCtrl = TextEditingController();
|
||||
final addressCtrl = TextEditingController();
|
||||
final pincodeCtrl = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final profile = Provider.of<UserProfileProvider>(
|
||||
context, listen: false).profile;
|
||||
|
||||
nameCtrl.text = profile?.customerName ?? '';
|
||||
companyCtrl.text = profile?.companyName ?? '';
|
||||
emailCtrl.text = profile?.email ?? '';
|
||||
mobileCtrl.text = profile?.mobile ?? '';
|
||||
addressCtrl.text = profile?.address ?? '';
|
||||
pincodeCtrl.text = profile?.pincode ?? '';
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
final provider =
|
||||
Provider.of<UserProfileProvider>(context, listen: false);
|
||||
|
||||
final data = {
|
||||
"customer_name": nameCtrl.text,
|
||||
"company_name": companyCtrl.text,
|
||||
"email": emailCtrl.text,
|
||||
"mobile_no": mobileCtrl.text,
|
||||
"address": addressCtrl.text,
|
||||
"pincode": pincodeCtrl.text,
|
||||
};
|
||||
|
||||
final success =
|
||||
await provider.sendProfileUpdateRequest(context, data);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(success
|
||||
? "Request submitted. Wait for admin approval."
|
||||
: "Failed to submit request")),
|
||||
);
|
||||
|
||||
if (success) Navigator.pop(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Edit Profile")),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(18),
|
||||
child: Column(
|
||||
children: [
|
||||
_field("Name", nameCtrl),
|
||||
_field("Company", companyCtrl),
|
||||
_field("Email", emailCtrl),
|
||||
_field("Mobile", mobileCtrl),
|
||||
_field("Address", addressCtrl),
|
||||
_field("Pincode", pincodeCtrl),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: _submit,
|
||||
child: const Text("Submit Update Request"),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _field(String title, TextEditingController ctrl) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 14),
|
||||
child: TextField(
|
||||
controller: ctrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: title,
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
169
lib/screens/invoice_detail_screen.dart
Normal file
169
lib/screens/invoice_detail_screen.dart
Normal file
@@ -0,0 +1,169 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/dio_client.dart';
|
||||
import '../services/invoice_service.dart';
|
||||
|
||||
class InvoiceDetailScreen extends StatefulWidget {
|
||||
final int invoiceId;
|
||||
const InvoiceDetailScreen({super.key, required this.invoiceId});
|
||||
|
||||
@override
|
||||
State<InvoiceDetailScreen> createState() => _InvoiceDetailScreenState();
|
||||
}
|
||||
|
||||
class _InvoiceDetailScreenState extends State<InvoiceDetailScreen> {
|
||||
bool loading = true;
|
||||
Map invoice = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
load();
|
||||
}
|
||||
|
||||
Future<void> load() async {
|
||||
final service = InvoiceService(DioClient.getInstance(context));
|
||||
final res = await service.getInvoiceDetails(widget.invoiceId);
|
||||
|
||||
if (res['success'] == true) {
|
||||
invoice = res['invoice'] ?? {};
|
||||
}
|
||||
|
||||
loading = false;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
/// ---------- REUSABLE ROW ----------
|
||||
Widget row(String label, dynamic value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label, style: const TextStyle(fontSize: 14, color: Colors.grey)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value?.toString().isNotEmpty == true ? value.toString() : "N/A",
|
||||
textAlign: TextAlign.end,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Invoice Details")),
|
||||
body: loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
|
||||
/// ================ INVOICE DATA ================
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ListView(
|
||||
children: [
|
||||
/// -------- Invoice Summary --------
|
||||
const Text(
|
||||
"Invoice Summary",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
row("Invoice No", invoice['invoice_number']),
|
||||
row("Invoice Date", invoice['invoice_date']),
|
||||
row("Due Date", invoice['due_date']),
|
||||
row("Status", invoice['status']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
/// -------- Customer Details --------
|
||||
const Text(
|
||||
"Customer Details",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
row("Name", invoice['customer_name']),
|
||||
row("Company", invoice['company_name']),
|
||||
row("Email", invoice['customer_email']),
|
||||
row("Mobile", invoice['customer_mobile']),
|
||||
row("Address", invoice['customer_address']),
|
||||
row("Pincode", invoice['pincode']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
/// -------- Amounts & Taxes --------
|
||||
const Text(
|
||||
"Amounts",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
row("Final Amount", invoice['final_amount']),
|
||||
row("Tax Type", invoice['tax_type']),
|
||||
row("GST %", invoice['gst_percent']),
|
||||
row("GST Amount", invoice['gst_amount']),
|
||||
row("Final with GST", invoice['final_amount_with_gst']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
/// -------- Payment Details --------
|
||||
const Text(
|
||||
"Payment Details",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
row("Payment Method", invoice['payment_method']),
|
||||
row("Reference No", invoice['reference_no']),
|
||||
row("Notes", invoice['notes']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
/// -------- PDF --------
|
||||
if (invoice['pdf_path'] != null)
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.picture_as_pdf),
|
||||
label: const Text("Download PDF"),
|
||||
onPressed: () {},
|
||||
style:
|
||||
ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
/// -------- Invoice Items --------
|
||||
if (invoice['items'] != null)
|
||||
const Text(
|
||||
"Invoice Items",
|
||||
style:
|
||||
TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
...List.generate(invoice['items']?.length ?? 0, (i) {
|
||||
final item = invoice['items'][i];
|
||||
return Card(
|
||||
child: ListTile(
|
||||
title: Text(item['description'] ?? "Item"),
|
||||
subtitle: Text("Qty: ${item['qty'] ?? 0}"),
|
||||
trailing: Text(
|
||||
"₹${item['ttl_amount'] ?? 0}",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.indigo),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
106
lib/screens/invoice_screen.dart
Normal file
106
lib/screens/invoice_screen.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/invoice_installment_screen.dart';
|
||||
import '../providers/invoice_provider.dart';
|
||||
import '../services/dio_client.dart';
|
||||
import '../services/invoice_service.dart';
|
||||
import 'invoice_detail_screen.dart';
|
||||
|
||||
|
||||
class InvoiceScreen extends StatefulWidget {
|
||||
const InvoiceScreen({super.key});
|
||||
|
||||
@override
|
||||
State<InvoiceScreen> createState() => _InvoiceScreenState();
|
||||
}
|
||||
|
||||
class _InvoiceScreenState extends State<InvoiceScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
Provider.of<InvoiceProvider>(context, listen: false)
|
||||
.loadInvoices(context);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = Provider.of<InvoiceProvider>(context);
|
||||
|
||||
if (provider.loading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (provider.invoices.isEmpty) {
|
||||
return const Center(
|
||||
child: Text("No invoices found", style: TextStyle(fontSize: 18)));
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: provider.invoices.length,
|
||||
itemBuilder: (_, i) {
|
||||
final inv = provider.invoices[i];
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Invoice ${inv['invoice_number'] ?? 'N/A'}",
|
||||
style: const TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
|
||||
const SizedBox(height: 6),
|
||||
Text("Date: ${inv['invoice_date'] ?? 'N/A'}"),
|
||||
Text("Status: ${inv['status'] ?? 'N/A'}"),
|
||||
Text("Amount: ₹${inv['formatted_amount'] ?? '0'}"),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
child: const Text("Invoice Details"),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => InvoiceDetailScreen(
|
||||
invoiceId: inv['invoice_id'],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
OutlinedButton(
|
||||
child: const Text("Installments"),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => InvoiceInstallmentScreen(
|
||||
invoiceId: inv['invoice_id'],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:kent_logistics_app/screens/welcome_screen.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
import '../widgets/rounded_input.dart';
|
||||
import '../widgets/primary_button.dart';
|
||||
import 'dashboard_screen.dart';
|
||||
import 'main_bottom_nav.dart';
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
const LoginScreen({super.key});
|
||||
@@ -25,21 +26,23 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
Future<void> _login() async {
|
||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||
|
||||
// Basic validation
|
||||
final loginId = cLoginId.text.trim();
|
||||
final password = cPassword.text.trim();
|
||||
|
||||
if (loginId.isEmpty || password.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please enter login id and password')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Please enter login id and password')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final res = await auth.login(loginId, password);
|
||||
final res = await auth.login(context, loginId, password);
|
||||
|
||||
// Your controller returns { success: true/false, message, token, user }
|
||||
if (res['success'] == true) {
|
||||
// Navigate to dashboard
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const DashboardScreen()));
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const MainBottomNav()),
|
||||
);
|
||||
} else {
|
||||
final msg = res['message']?.toString() ?? 'Login failed';
|
||||
if (!mounted) return;
|
||||
@@ -51,15 +54,35 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final auth = Provider.of<AuthProvider>(context);
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Login')),
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const WelcomeScreen()),
|
||||
);
|
||||
},
|
||||
),
|
||||
title: const Text('Login'),
|
||||
),
|
||||
|
||||
body: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: width * 0.06, vertical: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
RoundedInput(controller: cLoginId, hint: 'Email / Mobile / Customer ID', keyboardType: TextInputType.text),
|
||||
RoundedInput(
|
||||
controller: cLoginId,
|
||||
hint: 'Email / Mobile / Customer ID',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
RoundedInput(controller: cPassword, hint: 'Password', obscure: true),
|
||||
RoundedInput(
|
||||
controller: cPassword,
|
||||
hint: 'Password',
|
||||
obscure: true,
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
PrimaryButton(label: 'Login', onTap: _login, busy: auth.loading),
|
||||
],
|
||||
|
||||
56
lib/screens/main_bottom_nav.dart
Normal file
56
lib/screens/main_bottom_nav.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/main_app_bar.dart';
|
||||
import 'dashboard_screen.dart';
|
||||
import 'order_screen.dart';
|
||||
import 'invoice_screen.dart';
|
||||
import 'chat_screen.dart';
|
||||
import 'settings_screen.dart';
|
||||
|
||||
class MainBottomNav extends StatefulWidget {
|
||||
const MainBottomNav({super.key});
|
||||
|
||||
@override
|
||||
State<MainBottomNav> createState() => MainBottomNavState();
|
||||
}
|
||||
|
||||
class MainBottomNavState extends State<MainBottomNav> {
|
||||
int _currentIndex = 0;
|
||||
|
||||
void setIndex(int index) {
|
||||
setState(() {
|
||||
_currentIndex = index;
|
||||
});
|
||||
}
|
||||
|
||||
final List<Widget> _screens = const [
|
||||
DashboardScreen(),
|
||||
OrdersScreen(),
|
||||
InvoiceScreen(),
|
||||
ChatScreen(),
|
||||
SettingsScreen(),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: const MainAppBar(),
|
||||
body: _screens[_currentIndex],
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
currentIndex: _currentIndex,
|
||||
selectedItemColor: Colors.red,
|
||||
unselectedItemColor: Colors.black,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
onTap: (index) {
|
||||
setState(() => _currentIndex = index);
|
||||
},
|
||||
items: const [
|
||||
BottomNavigationBarItem(icon: Icon(Icons.dashboard_outlined), label: "Dashboard"),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.shopping_bag_outlined), label: "Orders"),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.receipt_long_outlined), label: "Invoice"),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.chat_bubble_outline), label: "Chat"),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.settings_outlined), label: "Settings"),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
56
lib/screens/mark_list_screen.dart
Normal file
56
lib/screens/mark_list_screen.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/mark_list_provider.dart';
|
||||
|
||||
class MarkListScreen extends StatefulWidget {
|
||||
const MarkListScreen({super.key});
|
||||
|
||||
@override
|
||||
State<MarkListScreen> createState() => _MarkListScreenState();
|
||||
}
|
||||
|
||||
class _MarkListScreenState extends State<MarkListScreen> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final provider = Provider.of<MarkListProvider>(context, listen: false);
|
||||
provider.init(context);
|
||||
provider.loadMarks(context); // Load full list again
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final marks = Provider.of<MarkListProvider>(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("All Mark Numbers"),
|
||||
),
|
||||
|
||||
body: marks.loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(12),
|
||||
itemCount: marks.marks.length,
|
||||
itemBuilder: (_, i) {
|
||||
final m = marks.marks[i];
|
||||
|
||||
return Card(
|
||||
child: ListTile(
|
||||
title: Text(m['mark_no']),
|
||||
subtitle: Text("${m['origin']} → ${m['destination']}"),
|
||||
trailing: Text(
|
||||
m['status'],
|
||||
style: const TextStyle(color: Colors.indigo),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
137
lib/screens/order_detail_screen.dart
Normal file
137
lib/screens/order_detail_screen.dart
Normal file
@@ -0,0 +1,137 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/dio_client.dart';
|
||||
import '../services/order_service.dart';
|
||||
|
||||
|
||||
class OrderDetailScreen extends StatefulWidget {
|
||||
final String orderId;
|
||||
const OrderDetailScreen({super.key, required this.orderId});
|
||||
|
||||
@override
|
||||
State<OrderDetailScreen> createState() => _OrderDetailScreenState();
|
||||
}
|
||||
|
||||
class _OrderDetailScreenState extends State<OrderDetailScreen> {
|
||||
bool loading = true;
|
||||
Map order = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
load();
|
||||
}
|
||||
|
||||
Future<void> load() async {
|
||||
final service = OrderService(DioClient.getInstance(context));
|
||||
final res = await service.getOrderDetails(widget.orderId);
|
||||
|
||||
if (res['success'] == true) {
|
||||
order = res['order'];
|
||||
}
|
||||
|
||||
loading = false;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Widget _row(String label, dynamic value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label,
|
||||
style: const TextStyle(fontSize: 14, color: Colors.grey)),
|
||||
Text(value?.toString() ?? 'N/A',
|
||||
style: const TextStyle(
|
||||
fontSize: 15, fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = order['items'] ?? [];
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Order Details")),
|
||||
body: loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ListView(
|
||||
children: [
|
||||
// ---------------- ORDER SUMMARY ----------------
|
||||
const Text(
|
||||
"Order Summary",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
_row("Order ID", order['order_id']),
|
||||
_row("Mark No", order['mark_no']),
|
||||
_row("Origin", order['origin']),
|
||||
_row("Destination", order['destination']),
|
||||
_row("Status", order['status']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
const Text(
|
||||
"Totals",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
_row("CTN", order['ctn']),
|
||||
_row("Qty", order['qty']),
|
||||
_row("Total Qty", order['ttl_qty']),
|
||||
_row("Amount", "₹${order['ttl_amount'] ?? 0}"),
|
||||
_row("CBM", order['cbm']),
|
||||
_row("Total CBM", order['ttl_cbm']),
|
||||
_row("KG", order['kg']),
|
||||
_row("Total KG", order['ttl_kg']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
// ---------------- ORDER ITEMS ----------------
|
||||
const Text(
|
||||
"Order Items",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
...List.generate(items.length, (i) {
|
||||
final item = items[i];
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item['description'] ?? "No description",
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
|
||||
_row("Qty", item['qty']),
|
||||
_row("Unit", item['unit']),
|
||||
_row("CBM", item['cbm']),
|
||||
_row("KG", item['kg']),
|
||||
_row("Amount", "₹${item['ttl_amount'] ?? 0}"),
|
||||
_row("Shop No", item['shop_no']),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
149
lib/screens/order_invoice_screen.dart
Normal file
149
lib/screens/order_invoice_screen.dart
Normal file
@@ -0,0 +1,149 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/dio_client.dart';
|
||||
import '../services/order_service.dart';
|
||||
|
||||
|
||||
class OrderInvoiceScreen extends StatefulWidget {
|
||||
final String orderId;
|
||||
const OrderInvoiceScreen({super.key, required this.orderId});
|
||||
|
||||
@override
|
||||
State<OrderInvoiceScreen> createState() => _OrderInvoiceScreenState();
|
||||
}
|
||||
|
||||
class _OrderInvoiceScreenState extends State<OrderInvoiceScreen> {
|
||||
bool loading = true;
|
||||
Map invoice = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
load();
|
||||
}
|
||||
|
||||
Future<void> load() async {
|
||||
final service = OrderService(DioClient.getInstance(context));
|
||||
final res = await service.getInvoice(widget.orderId);
|
||||
|
||||
if (res['success'] == true) {
|
||||
invoice = res['invoice'] ?? {};
|
||||
}
|
||||
|
||||
loading = false;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Widget _row(String label, dynamic value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label,
|
||||
style: const TextStyle(fontSize: 14, color: Colors.grey)),
|
||||
Text(value?.toString() ?? "N/A",
|
||||
style:
|
||||
const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = invoice['items'] ?? [];
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Invoice")),
|
||||
body: loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ListView(
|
||||
children: [
|
||||
const Text("Invoice Summary",
|
||||
style:
|
||||
TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
_row("Invoice No", invoice['invoice_number']),
|
||||
_row("Invoice Date", invoice['invoice_date']),
|
||||
_row("Due Date", invoice['due_date']),
|
||||
_row("Payment Method", invoice['payment_method']),
|
||||
_row("Reference No", invoice['reference_no']),
|
||||
_row("Status", invoice['status']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
const Text("Amount Details",
|
||||
style:
|
||||
TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
_row("Amount (without GST)", invoice['final_amount']),
|
||||
_row("GST Amount", invoice['gst_amount']),
|
||||
_row("Final Amount With GST",
|
||||
invoice['final_amount_with_gst']),
|
||||
_row("Tax Type", invoice['tax_type']),
|
||||
_row("CGST %", invoice['cgst_percent']),
|
||||
_row("SGST %", invoice['sgst_percent']),
|
||||
_row("IGST %", invoice['igst_percent']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
_row("Customer Name", invoice['customer_name']),
|
||||
_row("Company Name", invoice['company_name']),
|
||||
_row("Email", invoice['customer_email']),
|
||||
_row("Mobile", invoice['customer_mobile']),
|
||||
_row("Address", invoice['customer_address']),
|
||||
_row("Pincode", invoice['pincode']),
|
||||
_row("Notes", invoice['notes']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
// PDF DOWNLOAD
|
||||
if (invoice['pdf_path'] != null)
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
// open pdf
|
||||
},
|
||||
icon: const Icon(Icons.picture_as_pdf, color: Colors.red),
|
||||
label: const Text("Download PDF"),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
const Text("Invoice Items",
|
||||
style:
|
||||
TextStyle(fontSize: 17, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
...List.generate(items.length, (i) {
|
||||
final item = items[i];
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: ListTile(
|
||||
title: Text(item['description'] ?? "Item"),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Qty: ${item['qty'] ?? 0}"),
|
||||
Text("Price: ₹${item['price'] ?? 0}"),
|
||||
],
|
||||
),
|
||||
trailing: Text(
|
||||
"₹${item['ttl_amount'] ?? 0}",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.indigo),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
107
lib/screens/order_screen.dart
Normal file
107
lib/screens/order_screen.dart
Normal file
@@ -0,0 +1,107 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/order_provider.dart';
|
||||
import 'order_detail_screen.dart';
|
||||
import 'order_shipment_screen.dart';
|
||||
import 'order_invoice_screen.dart';
|
||||
import 'order_track_screen.dart';
|
||||
|
||||
class OrdersScreen extends StatefulWidget {
|
||||
const OrdersScreen({super.key});
|
||||
|
||||
@override
|
||||
State<OrdersScreen> createState() => _OrdersScreenState();
|
||||
}
|
||||
|
||||
class _OrdersScreenState extends State<OrdersScreen> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
Provider.of<OrderProvider>(context, listen: false).loadOrders();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = Provider.of<OrderProvider>(context);
|
||||
|
||||
if (provider.loading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: provider.orders.length,
|
||||
itemBuilder: (_, i) {
|
||||
final o = provider.orders[i];
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Order ID: ${o['order_id']}",
|
||||
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
Text(o['description']),
|
||||
Text("₹ ${o['amount']}"),
|
||||
Text(o['status'], style: const TextStyle(color: Colors.indigo)),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_btn("Order", () => _openOrderDetails(o['order_id'])),
|
||||
_btn("Shipment", () => _openShipment(o['order_id'])),
|
||||
_btn("Invoice", () => _openInvoice(o['order_id'])),
|
||||
_btn("Track", () => _openTrack(o['order_id'])),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _btn(String text, VoidCallback onTap) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
color: Colors.indigo.shade50,
|
||||
),
|
||||
child: Text(text, style: const TextStyle(color: Colors.indigo)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openOrderDetails(String id) {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (_) => OrderDetailScreen(orderId: id)));
|
||||
}
|
||||
|
||||
void _openShipment(String id) {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (_) => OrderShipmentScreen(orderId: id)));
|
||||
}
|
||||
|
||||
void _openInvoice(String id) {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (_) => OrderInvoiceScreen(orderId: id)));
|
||||
}
|
||||
|
||||
void _openTrack(String id) {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (_) => OrderTrackScreen(orderId: id)));
|
||||
}
|
||||
}
|
||||
184
lib/screens/order_shipment_screen.dart
Normal file
184
lib/screens/order_shipment_screen.dart
Normal file
@@ -0,0 +1,184 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/dio_client.dart';
|
||||
import '../services/order_service.dart';
|
||||
|
||||
|
||||
class OrderShipmentScreen extends StatefulWidget {
|
||||
final String orderId;
|
||||
const OrderShipmentScreen({super.key, required this.orderId});
|
||||
|
||||
@override
|
||||
State<OrderShipmentScreen> createState() => _OrderShipmentScreenState();
|
||||
}
|
||||
|
||||
class _OrderShipmentScreenState extends State<OrderShipmentScreen> {
|
||||
bool loading = true;
|
||||
Map? shipment; // nullable
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
load();
|
||||
}
|
||||
|
||||
Future<void> load() async {
|
||||
final service = OrderService(DioClient.getInstance(context));
|
||||
final res = await service.getShipment(widget.orderId);
|
||||
|
||||
if (res['success'] == true) {
|
||||
shipment = res['shipment']; // may be null
|
||||
}
|
||||
|
||||
loading = false;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Widget _row(String label, dynamic value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label,
|
||||
style: const TextStyle(fontSize: 14, color: Colors.grey)),
|
||||
Text(value?.toString() ?? "N/A",
|
||||
style: const TextStyle(
|
||||
fontSize: 15, fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Shipment Details")),
|
||||
body: loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
|
||||
// ---------------------------------------
|
||||
// 🚨 CASE 1: Shipment NOT created yet
|
||||
// ---------------------------------------
|
||||
: (shipment == null)
|
||||
? const Center(
|
||||
child: Text(
|
||||
"Order not shipped yet",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey),
|
||||
),
|
||||
)
|
||||
|
||||
// ---------------------------------------
|
||||
// 🚛 CASE 2: Shipment available
|
||||
// ---------------------------------------
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ListView(
|
||||
children: [
|
||||
const Text(
|
||||
"Shipment Summary",
|
||||
style: TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
_row("Shipment ID", shipment!['shipment_id']),
|
||||
_row("Status", shipment!['status']),
|
||||
_row("Shipment Date", shipment!['shipment_date']),
|
||||
_row("Origin", shipment!['origin']),
|
||||
_row("Destination", shipment!['destination']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
const Text(
|
||||
"Totals",
|
||||
style: TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
|
||||
_row("Total CTN", shipment!['total_ctn']),
|
||||
_row("Total Qty", shipment!['total_qty']),
|
||||
_row("Total TTL Qty", shipment!['total_ttl_qty']),
|
||||
_row("Total Amount", shipment!['total_amount']),
|
||||
_row("Total CBM", shipment!['total_cbm']),
|
||||
_row("Total TTL CBM", shipment!['total_ttl_cbm']),
|
||||
_row("Total KG", shipment!['total_kg']),
|
||||
_row("Total TTL KG", shipment!['total_ttl_kg']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
_row("Meta", shipment!['meta']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
const Text(
|
||||
"Shipment Items",
|
||||
style: TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
...List.generate(shipment!['items']?.length ?? 0, (i) {
|
||||
final item = shipment!['items'][i];
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
// 🔹 Title: Order ID
|
||||
Text(
|
||||
"Order ID: ${item['order_id'] ?? 'N/A'}",
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
|
||||
// 🔹 Mark No (optional)
|
||||
if (item['mark_no'] != null)
|
||||
Text(
|
||||
"Mark No: ${item['mark_no']}",
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
|
||||
const SizedBox(height: 6),
|
||||
|
||||
// 🔹 Total Quantity
|
||||
Text("Total Qty: ${item['total_ttl_qty'] ?? 0}"),
|
||||
|
||||
// 🔹 Total CBM (optional)
|
||||
if (item['total_ttl_cbm'] != null)
|
||||
Text("Total CBM: ${item['total_ttl_cbm']}"),
|
||||
|
||||
// 🔹 Total KG (optional)
|
||||
if (item['total_ttl_kg'] != null)
|
||||
Text("Total KG: ${item['total_ttl_kg']}"),
|
||||
|
||||
const SizedBox(height: 6),
|
||||
|
||||
// 🔹 Total Amount
|
||||
Text(
|
||||
"Amount: ₹${item['total_amount'] ?? 0}",
|
||||
style: const TextStyle(
|
||||
color: Colors.indigo,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
})
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
64
lib/screens/order_track_screen.dart
Normal file
64
lib/screens/order_track_screen.dart
Normal file
@@ -0,0 +1,64 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/dio_client.dart';
|
||||
import '../services/order_service.dart';
|
||||
|
||||
|
||||
class OrderTrackScreen extends StatefulWidget {
|
||||
final String orderId;
|
||||
const OrderTrackScreen({super.key, required this.orderId});
|
||||
|
||||
@override
|
||||
State<OrderTrackScreen> createState() => _OrderTrackScreenState();
|
||||
}
|
||||
|
||||
class _OrderTrackScreenState extends State<OrderTrackScreen> {
|
||||
bool loading = true;
|
||||
Map data = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
load();
|
||||
}
|
||||
|
||||
Future<void> load() async {
|
||||
final service = OrderService(DioClient.getInstance(context));
|
||||
final res = await service.trackOrder(widget.orderId);
|
||||
|
||||
if (res['success'] == true) {
|
||||
data = res['track'];
|
||||
}
|
||||
|
||||
loading = false;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Track Order")),
|
||||
body: loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Order ID: ${data['order_id']}"),
|
||||
Text("Shipment Status: ${data['shipment_status']}"),
|
||||
Text("Shipment Date: ${data['shipment_date']}"),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
Center(
|
||||
child: Icon(
|
||||
Icons.local_shipping,
|
||||
size: 100,
|
||||
color: Colors.indigo.shade300,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ class _OtpScreenState extends State<OtpScreen> {
|
||||
setState(() => verifying = true);
|
||||
|
||||
// send signup payload to backend
|
||||
final res = await RequestService().sendSignup(widget.signupPayload);
|
||||
final res = await RequestService(context).sendSignup(widget.signupPayload);
|
||||
|
||||
setState(() => verifying = false);
|
||||
|
||||
|
||||
193
lib/screens/settings_screen.dart
Normal file
193
lib/screens/settings_screen.dart
Normal file
@@ -0,0 +1,193 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
import '../providers/user_profile_provider.dart';
|
||||
import 'edit_profile_screen.dart';
|
||||
import 'login_screen.dart';
|
||||
|
||||
class SettingsScreen extends StatefulWidget {
|
||||
const SettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SettingsScreen> createState() => _SettingsScreenState();
|
||||
}
|
||||
|
||||
class _SettingsScreenState extends State<SettingsScreen> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||
|
||||
while (!auth.initialized) {
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
}
|
||||
|
||||
final profileProvider =
|
||||
Provider.of<UserProfileProvider>(context, listen: false);
|
||||
profileProvider.init(context);
|
||||
|
||||
await profileProvider.loadProfile(context);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _pickImage() async {
|
||||
final picked = await ImagePicker().pickImage(
|
||||
source: ImageSource.gallery,
|
||||
imageQuality: 80,
|
||||
);
|
||||
|
||||
if (picked != null) {
|
||||
final file = File(picked.path);
|
||||
final profileProvider =
|
||||
Provider.of<UserProfileProvider>(context, listen: false);
|
||||
|
||||
profileProvider.init(context);
|
||||
|
||||
final success = await profileProvider.updateProfileImage(context, file);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(success ? "Profile updated" : "Failed to update")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _logout() async {
|
||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: const Text("Logout"),
|
||||
content: const Text("Are you sure you want to logout?"),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text("Cancel"),
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
),
|
||||
TextButton(
|
||||
child: const Text("Logout", style: TextStyle(color: Colors.red)),
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirm == true) {
|
||||
await auth.logout(context);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const LoginScreen()),
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final profileProvider = Provider.of<UserProfileProvider>(context);
|
||||
|
||||
if (profileProvider.loading || profileProvider.profile == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final p = profileProvider.profile!;
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(18),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// ------------------ PROFILE IMAGE ------------------
|
||||
Center(
|
||||
child: GestureDetector(
|
||||
onTap: _pickImage,
|
||||
child: CircleAvatar(
|
||||
radius: 55,
|
||||
backgroundImage: p.profileImage != null
|
||||
? NetworkImage(p.profileImage!)
|
||||
: null,
|
||||
child: p.profileImage == null
|
||||
? const Icon(Icons.person, size: 55)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 25),
|
||||
|
||||
// ------------------ PROFILE INFO ------------------
|
||||
_infoRow("Customer ID", p.customerId),
|
||||
_infoRow("Name", p.customerName),
|
||||
_infoRow("Company", p.companyName),
|
||||
_infoRow("Email", p.email),
|
||||
_infoRow("Mobile", p.mobile),
|
||||
_infoRow("Address", p.address ?? "Not provided"),
|
||||
_infoRow("Pincode", p.pincode ?? "Not provided"),
|
||||
_infoRow("Status", p.status ?? "N/A"),
|
||||
_infoRow("Customer Type", p.customerType ?? "N/A"),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
Center(
|
||||
child: ElevatedButton(
|
||||
child: const Text("Edit Profile"),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const EditProfileScreen()),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// ------------------ LOGOUT BUTTON ------------------
|
||||
Center(
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 14),
|
||||
),
|
||||
icon: const Icon(Icons.logout, color: Colors.white),
|
||||
label: const Text(
|
||||
"Logout",
|
||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
onPressed: _logout,
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
|
||||
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _infoRow(String title, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey)),
|
||||
const SizedBox(height: 4),
|
||||
Text(value, style: const TextStyle(fontSize: 16)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,12 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
import 'dashboard_screen.dart';
|
||||
import 'main_bottom_nav.dart';
|
||||
import 'welcome_screen.dart';
|
||||
|
||||
class SplashScreen extends StatefulWidget {
|
||||
const SplashScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SplashScreen> createState() => _SplashScreenState();
|
||||
}
|
||||
@@ -19,15 +21,23 @@ class _SplashScreenState extends State<SplashScreen> {
|
||||
}
|
||||
|
||||
void _init() async {
|
||||
// small delay to show logo
|
||||
await Future.delayed(const Duration(milliseconds: 900));
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||
// ensure provider has loaded prefs
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
// 🟢 IMPORTANT → WAIT FOR PREFERENCES TO LOAD
|
||||
await auth.init();
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (auth.isLoggedIn) {
|
||||
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const DashboardScreen()));
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const MainBottomNav()),
|
||||
);
|
||||
} else {
|
||||
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const WelcomeScreen()));
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const WelcomeScreen()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,11 +50,23 @@ class _SplashScreenState extends State<SplashScreen> {
|
||||
Container(
|
||||
width: size.width * 0.34,
|
||||
height: size.width * 0.34,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: Theme.of(context).primaryColor.withOpacity(0.14)),
|
||||
child: Center(child: Text("K", style: TextStyle(fontSize: 48, fontWeight: FontWeight.bold, color: Theme.of(context).primaryColor))),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).primaryColor.withOpacity(0.14),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"K",
|
||||
style: TextStyle(
|
||||
fontSize: 48,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).primaryColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
const Text("Kent Logistics", style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)),
|
||||
const Text("Kent Logistics",
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)),
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
20
lib/services/dashboard_service.dart
Normal file
20
lib/services/dashboard_service.dart
Normal 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()};
|
||||
}
|
||||
}
|
||||
}
|
||||
33
lib/services/dio_client.dart
Normal file
33
lib/services/dio_client.dart
Normal 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!;
|
||||
}
|
||||
}
|
||||
34
lib/services/invoice_service.dart
Normal file
34
lib/services/invoice_service.dart
Normal 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()};
|
||||
}
|
||||
}
|
||||
}
|
||||
29
lib/services/mark_list_service.dart
Normal file
29
lib/services/mark_list_service.dart
Normal 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()};
|
||||
}
|
||||
}
|
||||
}
|
||||
32
lib/services/order_service.dart
Normal file
32
lib/services/order_service.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
37
lib/services/token_interceptor.dart
Normal file
37
lib/services/token_interceptor.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
48
lib/services/user_profile_service.dart
Normal file
48
lib/services/user_profile_service.dart
Normal 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()};
|
||||
}
|
||||
}
|
||||
}
|
||||
76
lib/widgets/main_app_bar.dart
Normal file
76
lib/widgets/main_app_bar.dart
Normal 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);
|
||||
}
|
||||
@@ -6,6 +6,10 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_linux
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import file_selector_macos
|
||||
import path_provider_foundation
|
||||
import shared_preferences_foundation
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
}
|
||||
|
||||
114
pubspec.lock
114
pubspec.lock
@@ -41,6 +41,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -97,6 +105,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -110,6 +150,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -144,6 +192,70 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -447,4 +559,4 @@ packages:
|
||||
version: "1.1.0"
|
||||
sdks:
|
||||
dart: ">=3.8.1 <4.0.0"
|
||||
flutter: ">=3.29.0"
|
||||
flutter: ">=3.32.0"
|
||||
|
||||
@@ -35,6 +35,8 @@ dependencies:
|
||||
provider: ^6.0.5
|
||||
shared_preferences: ^2.2.2
|
||||
google_fonts: ^4.0.3
|
||||
image_picker: ^1.0.7
|
||||
|
||||
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_windows
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
Reference in New Issue
Block a user