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 {
|
android {
|
||||||
namespace = "com.example.kent_logistics_app"
|
namespace = "com.example.kent_logistics_app"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = "27.0.12077973"
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<!-- 🔥 Permissions MUST be here (outside application) -->
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="kent_logistics_app"
|
android:label="kent_logistics_app"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -12,34 +19,29 @@
|
|||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
|
||||||
the Android process has started. This theme is visible to the user
|
|
||||||
while the Flutter UI initializes. After that, this theme continues
|
|
||||||
to determine the Window background behind the Flutter UI. -->
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
android:resource="@style/NormalTheme"
|
android:resource="@style/NormalTheme" />
|
||||||
/>
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
</application>
|
|
||||||
<!-- Required to query activities that can process text, see:
|
|
||||||
https://developer.android.com/training/package-visibility and
|
|
||||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
|
||||||
|
|
||||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
</application>
|
||||||
|
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
<data android:mimeType="text/plain"/>
|
<data android:mimeType="text/plain"/>
|
||||||
</intent>
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>App requires photo library access to update profile picture.</string>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>App requires camera access to take profile picture.</string>
|
||||||
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
class ApiConfig {
|
class ApiConfig {
|
||||||
// Android emulator (use 10.0.2.2), change for physical device or iOS simulator
|
static const String baseUrl = "http://10.207.50.74:8000/api";
|
||||||
static const String baseUrl = "http://10.0.2.2:8000/api";
|
|
||||||
}
|
}
|
||||||
|
|||||||
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:flutter/material.dart';
|
||||||
|
import 'package:kent_logistics_app/providers/dashboard_provider.dart';
|
||||||
|
import 'package:kent_logistics_app/providers/invoice_provider.dart';
|
||||||
|
import 'package:kent_logistics_app/providers/mark_list_provider.dart';
|
||||||
|
import 'package:kent_logistics_app/providers/order_provider.dart';
|
||||||
|
import 'package:kent_logistics_app/services/dio_client.dart';
|
||||||
|
import 'package:kent_logistics_app/services/order_service.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'providers/auth_provider.dart';
|
import 'providers/auth_provider.dart';
|
||||||
|
import 'providers/user_profile_provider.dart'; // NEW IMPORT
|
||||||
import 'screens/splash_screen.dart';
|
import 'screens/splash_screen.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
void main() {
|
void main() async {
|
||||||
runApp(const KentApp());
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
final auth = AuthProvider();
|
||||||
|
await auth.init(); // IMPORTANT: ensure prefs loaded before build
|
||||||
|
|
||||||
|
runApp(MultiProvider(
|
||||||
|
providers: [
|
||||||
|
ChangeNotifierProvider(create: (_) => auth),
|
||||||
|
ChangeNotifierProvider(create: (_) => UserProfileProvider()),
|
||||||
|
ChangeNotifierProvider(create: (_) => DashboardProvider()),
|
||||||
|
ChangeNotifierProvider(create: (_) => MarkListProvider()),
|
||||||
|
ChangeNotifierProvider(
|
||||||
|
create: (context) => OrderProvider(
|
||||||
|
OrderService(DioClient.getInstance(context)),
|
||||||
|
),
|
||||||
|
|
||||||
|
),
|
||||||
|
ChangeNotifierProvider(create: (_) => InvoiceProvider()),
|
||||||
|
],
|
||||||
|
child: const KentApp(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class KentApp extends StatelessWidget {
|
class KentApp extends StatelessWidget {
|
||||||
const KentApp({super.key});
|
const KentApp({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ChangeNotifierProvider(
|
|
||||||
create: (_) => AuthProvider(),
|
return MultiProvider(
|
||||||
|
providers: [
|
||||||
|
ChangeNotifierProvider(create: (_) => AuthProvider()),
|
||||||
|
ChangeNotifierProvider(create: (_) => UserProfileProvider()), // NEW
|
||||||
|
ChangeNotifierProvider(create: (_) => DashboardProvider()),
|
||||||
|
ChangeNotifierProvider(create: (_) => MarkListProvider()),
|
||||||
|
ChangeNotifierProvider(
|
||||||
|
create: (context) => OrderProvider(
|
||||||
|
OrderService(DioClient.getInstance(context)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ChangeNotifierProvider(create: (_) => InvoiceProvider()),
|
||||||
|
|
||||||
|
|
||||||
|
],
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
title: 'Kent Logistics',
|
title: 'Kent Logistics',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
textTheme: GoogleFonts.interTextTheme(),
|
textTheme: GoogleFonts.interTextTheme(),
|
||||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
|
colorScheme: ColorScheme.fromSeed(seedColor: Colors.red),
|
||||||
|
scaffoldBackgroundColor: const Color(0xfff8f6ff), // your light background
|
||||||
|
appBarTheme: const AppBarTheme(
|
||||||
|
backgroundColor: Colors.indigo, // FIX
|
||||||
|
foregroundColor: Colors.white, // white text + icons
|
||||||
|
elevation: 1,
|
||||||
|
centerTitle: true,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
home: const SplashScreen(),
|
home: const SplashScreen(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
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';
|
import '../services/auth_service.dart';
|
||||||
|
|
||||||
class AuthProvider extends ChangeNotifier {
|
class AuthProvider extends ChangeNotifier {
|
||||||
final AuthService _service = AuthService();
|
AuthService? _service;
|
||||||
|
|
||||||
bool _loading = false;
|
bool _loading = false;
|
||||||
bool get loading => _loading;
|
bool get loading => _loading;
|
||||||
|
|
||||||
|
bool initialized = false;
|
||||||
|
|
||||||
String? _token;
|
String? _token;
|
||||||
Map<String, dynamic>? _user;
|
Map<String, dynamic>? _user;
|
||||||
|
|
||||||
String? get token => _token;
|
String? get token => _token;
|
||||||
Map<String, dynamic>? get user => _user;
|
Map<String, dynamic>? get user => _user;
|
||||||
|
|
||||||
bool get isLoggedIn => _token != null && _token!.isNotEmpty;
|
bool get isLoggedIn => _token != null && _token!.isNotEmpty;
|
||||||
|
|
||||||
|
// Inject context after provider initializes
|
||||||
|
void initContext(BuildContext context) {
|
||||||
|
_service = AuthService(context);
|
||||||
|
}
|
||||||
|
|
||||||
AuthProvider() {
|
AuthProvider() {
|
||||||
_loadFromPrefs();
|
_loadFromPrefs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------- NEW FIX: SAFE INIT -----------------------
|
||||||
|
Future<void> init() async {
|
||||||
|
if (!initialized) {
|
||||||
|
await _loadFromPrefs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- LOAD FROM PREFS -----------------------
|
||||||
Future<void> _loadFromPrefs() async {
|
Future<void> _loadFromPrefs() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
_token = prefs.getString('token');
|
_token = prefs.getString('token');
|
||||||
|
|
||||||
final userJson = prefs.getString('user');
|
final userJson = prefs.getString('user');
|
||||||
if (userJson != null) {
|
if (userJson != null) {
|
||||||
try {
|
try {
|
||||||
@@ -31,22 +49,33 @@ class AuthProvider extends ChangeNotifier {
|
|||||||
_user = null;
|
_user = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initialized = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> login(String loginId, String password) async {
|
// -------------------------- LOGIN -----------------------------
|
||||||
|
Future<Map<String, dynamic>> login(
|
||||||
|
BuildContext context, String loginId, String password) async {
|
||||||
|
_service ??= AuthService(context);
|
||||||
|
|
||||||
_loading = true;
|
_loading = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
final res = await _service.login(loginId, password);
|
final res = await _service!.login(loginId, password);
|
||||||
|
|
||||||
_loading = false;
|
_loading = false;
|
||||||
|
|
||||||
if (res['success'] == true) {
|
if (res['success'] == true) {
|
||||||
final token = res['token']?.toString();
|
final token = res['token']?.toString();
|
||||||
final userMap = res['user'] is Map ? Map<String, dynamic>.from(res['user']) : null;
|
final userMap =
|
||||||
|
res['user'] is Map ? Map<String, dynamic>.from(res['user']) : null;
|
||||||
|
|
||||||
if (token != null && userMap != null) {
|
if (token != null && userMap != null) {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setString("saved_login_id", loginId);
|
||||||
|
await prefs.setString("saved_password", password);
|
||||||
|
|
||||||
await _saveSession(token, userMap);
|
await _saveSession(token, userMap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,27 +84,64 @@ class AuthProvider extends ChangeNotifier {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _saveSession(String token, Map<String, dynamic> userMap) async {
|
// --------------------- SAVE SESSION ---------------------------
|
||||||
|
Future<void> _saveSession(
|
||||||
|
String token, Map<String, dynamic> userMap) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setString('token', token);
|
await prefs.setString('token', token);
|
||||||
await prefs.setString('user', jsonEncode(userMap));
|
await prefs.setString('user', jsonEncode(userMap));
|
||||||
|
|
||||||
_token = token;
|
_token = token;
|
||||||
_user = userMap;
|
_user = userMap;
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> logout() async {
|
// ----------------------- LOGOUT -------------------------------
|
||||||
// optional: call API logout if implemented
|
Future<void> logout(BuildContext context) async {
|
||||||
if (_token != null) {
|
_service ??= AuthService(context);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _service.logout(_token!);
|
await _service!.logout();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
|
||||||
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.remove('token');
|
await prefs.remove('token');
|
||||||
await prefs.remove('user');
|
await prefs.remove('user');
|
||||||
|
|
||||||
_token = null;
|
_token = null;
|
||||||
_user = null;
|
_user = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------- AUTO LOGIN ------------------------------
|
||||||
|
Future<bool> autoLoginFromSavedCredentials(BuildContext context) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final loginId = prefs.getString('saved_login_id');
|
||||||
|
final password = prefs.getString('saved_password');
|
||||||
|
|
||||||
|
if (loginId == null || password == null) return false;
|
||||||
|
|
||||||
|
final res = await login(context, loginId, password);
|
||||||
|
return res['success'] == true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------- REFRESH TOKEN --------------------------
|
||||||
|
Future<bool> tryRefreshToken(BuildContext context) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final oldToken = prefs.getString('token');
|
||||||
|
|
||||||
|
if (oldToken == null) return false;
|
||||||
|
|
||||||
|
_service ??= AuthService(context);
|
||||||
|
|
||||||
|
final res = await _service!.refreshToken(oldToken);
|
||||||
|
|
||||||
|
if (res['success'] == true && res['token'] != null) {
|
||||||
|
await prefs.setString('token', res['token']);
|
||||||
|
_token = res['token'];
|
||||||
|
notifyListeners();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../providers/auth_provider.dart';
|
import '../providers/auth_provider.dart';
|
||||||
import 'welcome_screen.dart';
|
import '../providers/dashboard_provider.dart';
|
||||||
|
import '../providers/mark_list_provider.dart';
|
||||||
|
import 'mark_list_screen.dart';
|
||||||
|
|
||||||
class DashboardScreen extends StatelessWidget {
|
class DashboardScreen extends StatefulWidget {
|
||||||
const DashboardScreen({super.key});
|
const DashboardScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DashboardScreen> createState() => _DashboardScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DashboardScreenState extends State<DashboardScreen> {
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||||
|
|
||||||
|
// STEP 1: Try refresh token BEFORE any API calls
|
||||||
|
await auth.tryRefreshToken(context);
|
||||||
|
|
||||||
|
// STEP 2: Now safe to load dashboard
|
||||||
|
final dash = Provider.of<DashboardProvider>(context, listen: false);
|
||||||
|
dash.init(context);
|
||||||
|
await dash.loadSummary(context);
|
||||||
|
|
||||||
|
// STEP 3: Load marks AFTER refresh
|
||||||
|
final marks = Provider.of<MarkListProvider>(context, listen: false);
|
||||||
|
marks.init(context);
|
||||||
|
await marks.loadMarks(context);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void _showAddMarkForm() {
|
||||||
|
final markCtrl = TextEditingController();
|
||||||
|
final originCtrl = TextEditingController();
|
||||||
|
final destCtrl = TextEditingController();
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
|
||||||
|
builder: (_) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
18,
|
||||||
|
18,
|
||||||
|
18,
|
||||||
|
MediaQuery.of(context).viewInsets.bottom + 20,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"Add Mark No",
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
TextField(controller: markCtrl, decoration: const InputDecoration(labelText: "Mark No")),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
TextField(controller: originCtrl, decoration: const InputDecoration(labelText: "Origin")),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
TextField(controller: destCtrl, decoration: const InputDecoration(labelText: "Destination")),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final mark = markCtrl.text.trim();
|
||||||
|
final origin = originCtrl.text.trim();
|
||||||
|
final dest = destCtrl.text.trim();
|
||||||
|
|
||||||
|
if (mark.isEmpty || origin.isEmpty || dest.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(const SnackBar(content: Text("All fields required")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final provider = Provider.of<MarkListProvider>(context, listen: false);
|
||||||
|
final res = await provider.addMark(context, mark, origin, dest);
|
||||||
|
|
||||||
|
if (res['success'] == true) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
} else {
|
||||||
|
final msg = res['message'] ?? "Failed";
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text("Submit"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final auth = Provider.of<AuthProvider>(context);
|
final auth = Provider.of<AuthProvider>(context);
|
||||||
|
final dash = Provider.of<DashboardProvider>(context);
|
||||||
|
final marks = Provider.of<MarkListProvider>(context);
|
||||||
|
|
||||||
final name = auth.user?['customer_name'] ?? 'User';
|
final name = auth.user?['customer_name'] ?? 'User';
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
if (dash.loading) {
|
||||||
title: const Text("Dashboard"),
|
return const Center(child: CircularProgressIndicator());
|
||||||
actions: [
|
}
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.logout),
|
return SingleChildScrollView(
|
||||||
onPressed: () async {
|
padding: const EdgeInsets.all(18),
|
||||||
await auth.logout();
|
child: Column(
|
||||||
Navigator.of(context).pushAndRemoveUntil(MaterialPageRoute(builder: (_) => const WelcomeScreen()), (r) => false);
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
},
|
children: [
|
||||||
)
|
// HEADER
|
||||||
|
Text(
|
||||||
|
"Welcome, $name 👋",
|
||||||
|
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// ORDER SUMMARY
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
_statBox("Active", dash.activeOrders, Colors.blue),
|
||||||
|
_statBox("In Transit", dash.inTransitOrders, Colors.orange),
|
||||||
|
_statBox("Delivered", dash.deliveredOrders, Colors.green),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Padding(
|
const SizedBox(height: 20),
|
||||||
|
_valueCard("Total Value", dash.totalValue),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
_valueCard("Raw Amount", "₹${dash.totalRaw.toStringAsFixed(2)}"),
|
||||||
|
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
|
// ADD + VIEW ALL BUTTONS SIDE BY SIDE
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text("Add Mark No"),
|
||||||
|
onPressed: _showAddMarkForm,
|
||||||
|
),
|
||||||
|
|
||||||
|
if (marks.marks.length > 0)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (_) => const MarkListScreen()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
"View All →",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.indigo,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// MARK LIST (only 10 latest)
|
||||||
|
const Text(
|
||||||
|
"Latest Mark Numbers",
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
if (marks.loading)
|
||||||
|
const Center(child: CircularProgressIndicator())
|
||||||
|
else
|
||||||
|
Column(
|
||||||
|
children: List.generate(
|
||||||
|
marks.marks.length > 10 ? 10 : marks.marks.length,
|
||||||
|
(i) {
|
||||||
|
final m = marks.marks[i];
|
||||||
|
return Card(
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(m['mark_no']),
|
||||||
|
subtitle: Text("${m['origin']} → ${m['destination']}"),
|
||||||
|
trailing: Text(
|
||||||
|
m['status'],
|
||||||
|
style: const TextStyle(color: Colors.indigo),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI WIDGETS
|
||||||
|
Widget _statBox(String title, int value, Color color) {
|
||||||
|
return Container(
|
||||||
|
width: 110,
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withOpacity(0.15),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(value.toString(),
|
||||||
|
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(title, style: const TextStyle(fontSize: 14)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _valueCard(String title, String value) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(18),
|
padding: const EdgeInsets.all(18),
|
||||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
decoration: BoxDecoration(
|
||||||
Text("Welcome, ${name}", style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
|
color: Colors.indigo.shade50,
|
||||||
const SizedBox(height: 12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
const Text("Your dashboard will appear here. Build shipment list, tracking, create shipment screens next."),
|
),
|
||||||
]),
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14, fontWeight: FontWeight.w600, color: Colors.grey)),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.indigo,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
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:flutter/material.dart';
|
||||||
|
import 'package:kent_logistics_app/screens/welcome_screen.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../providers/auth_provider.dart';
|
import '../providers/auth_provider.dart';
|
||||||
import '../widgets/rounded_input.dart';
|
import '../widgets/rounded_input.dart';
|
||||||
import '../widgets/primary_button.dart';
|
import '../widgets/primary_button.dart';
|
||||||
import 'dashboard_screen.dart';
|
import 'main_bottom_nav.dart';
|
||||||
|
|
||||||
class LoginScreen extends StatefulWidget {
|
class LoginScreen extends StatefulWidget {
|
||||||
const LoginScreen({super.key});
|
const LoginScreen({super.key});
|
||||||
@@ -25,21 +26,23 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
Future<void> _login() async {
|
Future<void> _login() async {
|
||||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||||
|
|
||||||
// Basic validation
|
|
||||||
final loginId = cLoginId.text.trim();
|
final loginId = cLoginId.text.trim();
|
||||||
final password = cPassword.text.trim();
|
final password = cPassword.text.trim();
|
||||||
|
|
||||||
if (loginId.isEmpty || password.isEmpty) {
|
if (loginId.isEmpty || password.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please enter login id and password')));
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Please enter login id and password')),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final res = await auth.login(loginId, password);
|
final res = await auth.login(context, loginId, password);
|
||||||
|
|
||||||
// Your controller returns { success: true/false, message, token, user }
|
|
||||||
if (res['success'] == true) {
|
if (res['success'] == true) {
|
||||||
// Navigate to dashboard
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const DashboardScreen()));
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(builder: (_) => const MainBottomNav()),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
final msg = res['message']?.toString() ?? 'Login failed';
|
final msg = res['message']?.toString() ?? 'Login failed';
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -51,15 +54,35 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final auth = Provider.of<AuthProvider>(context);
|
final auth = Provider.of<AuthProvider>(context);
|
||||||
final width = MediaQuery.of(context).size.width;
|
final width = MediaQuery.of(context).size.width;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Login')),
|
appBar: AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pushReplacement(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (_) => const WelcomeScreen()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: const Text('Login'),
|
||||||
|
),
|
||||||
|
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: width * 0.06, vertical: 20),
|
padding: EdgeInsets.symmetric(horizontal: width * 0.06, vertical: 20),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
RoundedInput(controller: cLoginId, hint: 'Email / Mobile / Customer ID', keyboardType: TextInputType.text),
|
RoundedInput(
|
||||||
|
controller: cLoginId,
|
||||||
|
hint: 'Email / Mobile / Customer ID',
|
||||||
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
RoundedInput(controller: cPassword, hint: 'Password', obscure: true),
|
RoundedInput(
|
||||||
|
controller: cPassword,
|
||||||
|
hint: 'Password',
|
||||||
|
obscure: true,
|
||||||
|
),
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
PrimaryButton(label: 'Login', onTap: _login, busy: auth.loading),
|
PrimaryButton(label: 'Login', onTap: _login, busy: auth.loading),
|
||||||
],
|
],
|
||||||
|
|||||||
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);
|
setState(() => verifying = true);
|
||||||
|
|
||||||
// send signup payload to backend
|
// send signup payload to backend
|
||||||
final res = await RequestService().sendSignup(widget.signupPayload);
|
final res = await RequestService(context).sendSignup(widget.signupPayload);
|
||||||
|
|
||||||
setState(() => verifying = false);
|
setState(() => verifying = false);
|
||||||
|
|
||||||
|
|||||||
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 'package:provider/provider.dart';
|
||||||
import '../providers/auth_provider.dart';
|
import '../providers/auth_provider.dart';
|
||||||
import 'dashboard_screen.dart';
|
import 'dashboard_screen.dart';
|
||||||
|
import 'main_bottom_nav.dart';
|
||||||
import 'welcome_screen.dart';
|
import 'welcome_screen.dart';
|
||||||
|
|
||||||
class SplashScreen extends StatefulWidget {
|
class SplashScreen extends StatefulWidget {
|
||||||
const SplashScreen({super.key});
|
const SplashScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SplashScreen> createState() => _SplashScreenState();
|
State<SplashScreen> createState() => _SplashScreenState();
|
||||||
}
|
}
|
||||||
@@ -19,15 +21,23 @@ class _SplashScreenState extends State<SplashScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _init() async {
|
void _init() async {
|
||||||
// small delay to show logo
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
await Future.delayed(const Duration(milliseconds: 900));
|
|
||||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||||
// ensure provider has loaded prefs
|
|
||||||
await Future.delayed(const Duration(milliseconds: 300));
|
// 🟢 IMPORTANT → WAIT FOR PREFERENCES TO LOAD
|
||||||
|
await auth.init();
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
if (auth.isLoggedIn) {
|
if (auth.isLoggedIn) {
|
||||||
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const DashboardScreen()));
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(builder: (_) => const MainBottomNav()),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const WelcomeScreen()));
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(builder: (_) => const WelcomeScreen()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,11 +50,23 @@ class _SplashScreenState extends State<SplashScreen> {
|
|||||||
Container(
|
Container(
|
||||||
width: size.width * 0.34,
|
width: size.width * 0.34,
|
||||||
height: size.width * 0.34,
|
height: size.width * 0.34,
|
||||||
decoration: BoxDecoration(shape: BoxShape.circle, color: Theme.of(context).primaryColor.withOpacity(0.14)),
|
decoration: BoxDecoration(
|
||||||
child: Center(child: Text("K", style: TextStyle(fontSize: 48, fontWeight: FontWeight.bold, color: Theme.of(context).primaryColor))),
|
shape: BoxShape.circle,
|
||||||
|
color: Theme.of(context).primaryColor.withOpacity(0.14),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
"K",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 48,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).primaryColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
const Text("Kent Logistics", style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)),
|
const Text("Kent Logistics",
|
||||||
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import '../config/api_config.dart';
|
import '../config/api_config.dart';
|
||||||
|
import 'dio_client.dart';
|
||||||
|
|
||||||
class AuthService {
|
class AuthService {
|
||||||
final Dio _dio = Dio(BaseOptions(
|
late final Dio _dio;
|
||||||
baseUrl: ApiConfig.baseUrl,
|
|
||||||
connectTimeout: const Duration(seconds: 15),
|
|
||||||
receiveTimeout: const Duration(seconds: 15),
|
|
||||||
// You can add headers here if needed:
|
|
||||||
// headers: {'Accept': 'application/json'},
|
|
||||||
));
|
|
||||||
|
|
||||||
/// Calls /api/user/login with login_id and password
|
AuthService(BuildContext context) {
|
||||||
|
_dio = DioClient.getInstance(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Login API
|
||||||
Future<Map<String, dynamic>> login(String loginId, String password) async {
|
Future<Map<String, dynamic>> login(String loginId, String password) async {
|
||||||
try {
|
try {
|
||||||
final response = await _dio.post('/user/login', data: {
|
final response = await _dio.post('/user/login', data: {
|
||||||
@@ -18,45 +18,44 @@ class AuthService {
|
|||||||
'password': password,
|
'password': password,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ensure we return a Map<String, dynamic>
|
|
||||||
if (response.data is Map) {
|
|
||||||
return Map<String, dynamic>.from(response.data);
|
return Map<String, dynamic>.from(response.data);
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
'success': false,
|
|
||||||
'message': 'Invalid response from server',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
// Try to extract message from server response
|
final data = e.response?.data;
|
||||||
dynamic respData = e.response?.data;
|
|
||||||
String message = 'Login failed';
|
|
||||||
if (respData is Map && respData['message'] != null) {
|
|
||||||
message = respData['message'].toString();
|
|
||||||
} else if (e.message != null) {
|
|
||||||
message = e.message!;
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
'success': false,
|
'success': false,
|
||||||
'message': message,
|
'message': data is Map && data['message'] != null
|
||||||
|
? data['message']
|
||||||
|
: e.message ?? 'Login failed'
|
||||||
};
|
};
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
'success': false,
|
|
||||||
'message': e.toString(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Optional: logout (if you have logout endpoint)
|
|
||||||
Future<Map<String, dynamic>> logout(String token) async {
|
|
||||||
try {
|
|
||||||
final Dio dio = Dio(BaseOptions(baseUrl: ApiConfig.baseUrl));
|
|
||||||
dio.options.headers['Authorization'] = 'Bearer $token';
|
|
||||||
final response = await dio.post('/user/logout');
|
|
||||||
return Map<String, dynamic>.from(response.data ?? {'success': true});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {'success': false, 'message': e.toString()};
|
return {'success': false, 'message': e.toString()};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Logout API
|
||||||
|
Future<Map<String, dynamic>> logout() async {
|
||||||
|
try {
|
||||||
|
final response = await _dio.post('/user/logout');
|
||||||
|
return Map<String, dynamic>.from(response.data);
|
||||||
|
} catch (e) {
|
||||||
|
return {'success': false, 'message': e.toString()};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refresh token
|
||||||
|
Future<Map<String, dynamic>> refreshToken(String oldToken) async {
|
||||||
|
try {
|
||||||
|
final response = await _dio.post(
|
||||||
|
'/user/refresh',
|
||||||
|
options: Options(headers: {
|
||||||
|
'Authorization': 'Bearer $oldToken',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Map<String, dynamic>.from(response.data);
|
||||||
|
} on DioException catch (e) {
|
||||||
|
final msg = e.response?.data?['message'] ?? 'Refresh failed';
|
||||||
|
return {'success': false, 'message': msg};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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 'package:dio/dio.dart';
|
||||||
import '../config/api_config.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'dio_client.dart';
|
||||||
|
|
||||||
class RequestService {
|
class RequestService {
|
||||||
final Dio _dio = Dio(
|
late final Dio _dio;
|
||||||
BaseOptions(
|
|
||||||
baseUrl: ApiConfig.baseUrl,
|
RequestService(BuildContext context) {
|
||||||
connectTimeout: const Duration(seconds: 15),
|
_dio = DioClient.getInstance(context);
|
||||||
),
|
}
|
||||||
);
|
|
||||||
/// Send signup request to backend (after OTP verified)
|
/// Signup request after OTP
|
||||||
Future<Map<String, dynamic>> sendSignup(Map<String, dynamic> payload) async {
|
Future<Map<String, dynamic>> sendSignup(Map<String, dynamic> payload) async {
|
||||||
try {
|
try {
|
||||||
final resp = await _dio.post('/signup-request', data: payload);
|
final resp = await _dio.post('/signup-request', data: payload);
|
||||||
return resp.data is Map ? Map<String, dynamic>.from(resp.data) : {'status': true, 'message': 'OK'};
|
return Map<String, dynamic>.from(resp.data);
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
return {
|
return {
|
||||||
'status': false,
|
'status': false,
|
||||||
|
|||||||
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 "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
|
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||||
|
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
file_selector_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
|||||||
@@ -5,10 +5,12 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import file_selector_macos
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
114
pubspec.lock
114
pubspec.lock
@@ -41,6 +41,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
version: "1.19.1"
|
||||||
|
cross_file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cross_file
|
||||||
|
sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.5+1"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -97,6 +105,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.1"
|
version: "7.0.1"
|
||||||
|
file_selector_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_linux
|
||||||
|
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.4"
|
||||||
|
file_selector_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_macos
|
||||||
|
sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.4+4"
|
||||||
|
file_selector_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_platform_interface
|
||||||
|
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
|
file_selector_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_windows
|
||||||
|
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.3+5"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -110,6 +150,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0"
|
version: "5.0.0"
|
||||||
|
flutter_plugin_android_lifecycle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_plugin_android_lifecycle
|
||||||
|
sha256: c2fe1001710127dfa7da89977a08d591398370d099aacdaa6d44da7eb14b8476
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.31"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -144,6 +192,70 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
version: "4.1.2"
|
||||||
|
image_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: image_picker
|
||||||
|
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
|
image_picker_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_android
|
||||||
|
sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.13+1"
|
||||||
|
image_picker_for_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_for_web
|
||||||
|
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.1"
|
||||||
|
image_picker_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_ios
|
||||||
|
sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.13"
|
||||||
|
image_picker_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_linux
|
||||||
|
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2"
|
||||||
|
image_picker_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_macos
|
||||||
|
sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2"
|
||||||
|
image_picker_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_platform_interface
|
||||||
|
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.11.1"
|
||||||
|
image_picker_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_windows
|
||||||
|
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -447,4 +559,4 @@ packages:
|
|||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.8.1 <4.0.0"
|
dart: ">=3.8.1 <4.0.0"
|
||||||
flutter: ">=3.29.0"
|
flutter: ">=3.32.0"
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ dependencies:
|
|||||||
provider: ^6.0.5
|
provider: ^6.0.5
|
||||||
shared_preferences: ^2.2.2
|
shared_preferences: ^2.2.2
|
||||||
google_fonts: ^4.0.3
|
google_fonts: ^4.0.3
|
||||||
|
image_picker: ^1.0.7
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
file_selector_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
|||||||
Reference in New Issue
Block a user