Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98d184d901 | ||
|
|
0c1d3b8cb2 | ||
|
|
d419e4ed60 | ||
|
|
8dac57a3a8 | ||
|
|
e85ac4bf8c | ||
|
|
d606156a6d | ||
|
|
b9fb9455e7 | ||
|
|
bbde34fae4 | ||
|
|
bb81269140 | ||
|
|
9faf983b95 |
File diff suppressed because one or more lines are too long
3
.gitignore
vendored
3
.gitignore
vendored
@@ -32,3 +32,6 @@ windows/flutter/ephemeral/
|
|||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# FVM Version Cache
|
||||||
|
.fvm/
|
||||||
@@ -4,11 +4,20 @@
|
|||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="32" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_DOCUMENTS" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<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"
|
||||||
|
android:usesCleartextTraffic="true">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
|||||||
@@ -1,5 +1,37 @@
|
|||||||
package com.example.kent_logistics_app
|
package com.example.kent_logistics_app
|
||||||
|
|
||||||
|
import android.media.MediaScannerConnection
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
|
||||||
class MainActivity : FlutterActivity()
|
class MainActivity : FlutterActivity() {
|
||||||
|
|
||||||
|
private val CHANNEL = "media_scanner"
|
||||||
|
|
||||||
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
|
super.configureFlutterEngine(flutterEngine)
|
||||||
|
|
||||||
|
MethodChannel(
|
||||||
|
flutterEngine.dartExecutor.binaryMessenger,
|
||||||
|
CHANNEL
|
||||||
|
).setMethodCallHandler { call, result ->
|
||||||
|
if (call.method == "scanFile") {
|
||||||
|
val path = call.argument<String>("path")
|
||||||
|
|
||||||
|
if (path != null) {
|
||||||
|
MediaScannerConnection.scanFile(
|
||||||
|
applicationContext,
|
||||||
|
arrayOf(path),
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.success(null)
|
||||||
|
} else {
|
||||||
|
result.notImplemented()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
BIN
assets/Images/K.png
Normal file
BIN
assets/Images/K.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -1,3 +1,8 @@
|
|||||||
|
// class ApiConfig {
|
||||||
|
// static const String baseUrl = "http://103.248.30.24:3030/api";
|
||||||
|
// static const String fileBaseUrl = "http://103.248.30.24:3030/";
|
||||||
|
// }
|
||||||
class ApiConfig {
|
class ApiConfig {
|
||||||
static const String baseUrl = "http://10.207.50.74:8000/api";
|
static const String baseUrl = "http://10.119.0.74:8000/api";
|
||||||
|
static const String fileBaseUrl = "http://10.119.0.74:8000/";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class AppConfig {
|
|||||||
static const String logoUrlEmulator = "http://10.0.2.2:8000/images/kent_logo2.png";
|
static const String logoUrlEmulator = "http://10.0.2.2:8000/images/kent_logo2.png";
|
||||||
|
|
||||||
// For Physical Device (Replace with your actual PC local IP)
|
// For Physical Device (Replace with your actual PC local IP)
|
||||||
static const String logoUrlDevice = "http://10.207.50.74:8000/images/kent_logo2.png";
|
static const String logoUrlDevice = "http://103.248.30.24:8000/images/kent_logo2.png";
|
||||||
|
|
||||||
// Which one to use?
|
// Which one to use?
|
||||||
static const String logoUrl = logoUrlDevice; // CHANGE THIS WHEN TESTING ON REAL DEVICE
|
static const String logoUrl = logoUrlDevice; // CHANGE THIS WHEN TESTING ON REAL DEVICE
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:kent_logistics_app/providers/chat_unread_provider.dart';
|
||||||
import 'package:kent_logistics_app/providers/dashboard_provider.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/invoice_provider.dart';
|
||||||
import 'package:kent_logistics_app/providers/mark_list_provider.dart';
|
import 'package:kent_logistics_app/providers/mark_list_provider.dart';
|
||||||
@@ -30,6 +31,7 @@ void main() async {
|
|||||||
|
|
||||||
),
|
),
|
||||||
ChangeNotifierProvider(create: (_) => InvoiceProvider()),
|
ChangeNotifierProvider(create: (_) => InvoiceProvider()),
|
||||||
|
ChangeNotifierProvider(create: (_) => ChatUnreadProvider()),
|
||||||
],
|
],
|
||||||
child: const KentApp(),
|
child: const KentApp(),
|
||||||
));
|
));
|
||||||
@@ -54,7 +56,7 @@ class KentApp extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
ChangeNotifierProvider(create: (_) => InvoiceProvider()),
|
ChangeNotifierProvider(create: (_) => InvoiceProvider()),
|
||||||
|
ChangeNotifierProvider(create: (_) => ChatUnreadProvider()),
|
||||||
|
|
||||||
],
|
],
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
@@ -64,7 +66,7 @@ class KentApp extends StatelessWidget {
|
|||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
textTheme: GoogleFonts.interTextTheme(),
|
textTheme: GoogleFonts.interTextTheme(),
|
||||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.red),
|
colorScheme: ColorScheme.fromSeed(seedColor: Colors.red),
|
||||||
scaffoldBackgroundColor: const Color(0xfff8f6ff), // your light background
|
scaffoldBackgroundColor: const Color(0xFFE8F0FF), // your light background
|
||||||
appBarTheme: const AppBarTheme(
|
appBarTheme: const AppBarTheme(
|
||||||
backgroundColor: Colors.indigo, // FIX
|
backgroundColor: Colors.indigo, // FIX
|
||||||
foregroundColor: Colors.white, // white text + icons
|
foregroundColor: Colors.white, // white text + icons
|
||||||
|
|||||||
@@ -125,23 +125,27 @@ class AuthProvider extends ChangeNotifier {
|
|||||||
return res['success'] == true;
|
return res['success'] == true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------- REFRESH TOKEN --------------------------
|
Future<void> forceLogout(BuildContext context) async {
|
||||||
Future<bool> tryRefreshToken(BuildContext context) async {
|
debugPrint('🚪🚪🚪 [AUTH] Force logout triggered');
|
||||||
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final oldToken = prefs.getString('token');
|
|
||||||
|
|
||||||
if (oldToken == null) return false;
|
await prefs.remove('token');
|
||||||
|
await prefs.remove('user');
|
||||||
|
await prefs.remove('saved_login_id');
|
||||||
|
await prefs.remove('saved_password');
|
||||||
|
|
||||||
_service ??= AuthService(context);
|
_token = null;
|
||||||
|
_user = null;
|
||||||
|
|
||||||
final res = await _service!.refreshToken(oldToken);
|
|
||||||
|
|
||||||
if (res['success'] == true && res['token'] != null) {
|
|
||||||
await prefs.setString('token', res['token']);
|
|
||||||
_token = res['token'];
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return true;
|
|
||||||
}
|
// Redirect to login & clear navigation stack
|
||||||
return false;
|
Navigator.of(context).pushNamedAndRemoveUntil(
|
||||||
|
'/login',
|
||||||
|
(route) => false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
34
lib/providers/chat_unread_provider.dart
Normal file
34
lib/providers/chat_unread_provider.dart
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ChatUnreadProvider extends ChangeNotifier {
|
||||||
|
int _unreadCount = 0;
|
||||||
|
bool _chatOpen = false;
|
||||||
|
|
||||||
|
int get unreadCount => _unreadCount;
|
||||||
|
bool get isChatOpen => _chatOpen;
|
||||||
|
|
||||||
|
/// 📩 Called when ADMIN sends message
|
||||||
|
void increment() {
|
||||||
|
if (!_chatOpen) {
|
||||||
|
_unreadCount++;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 👁 Called when chat screen is opened
|
||||||
|
void setChatOpen(bool open) {
|
||||||
|
_chatOpen = open;
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
_unreadCount = 0; // reset badge when user opens chat
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🔁 Manual reset (optional)
|
||||||
|
void reset() {
|
||||||
|
_unreadCount = 0;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,11 @@ import '../services/invoice_service.dart';
|
|||||||
|
|
||||||
class InvoiceInstallmentScreen extends StatefulWidget {
|
class InvoiceInstallmentScreen extends StatefulWidget {
|
||||||
final int invoiceId;
|
final int invoiceId;
|
||||||
const InvoiceInstallmentScreen({super.key, required this.invoiceId});
|
|
||||||
|
const InvoiceInstallmentScreen({
|
||||||
|
super.key,
|
||||||
|
required this.invoiceId,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<InvoiceInstallmentScreen> createState() =>
|
State<InvoiceInstallmentScreen> createState() =>
|
||||||
@@ -35,39 +39,188 @@ class _InvoiceInstallmentScreenState extends State<InvoiceInstallmentScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final width = MediaQuery.of(context).size.width;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text("Installments")),
|
backgroundColor: Colors.grey.shade100,
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Installments"),
|
||||||
|
elevation: 1,
|
||||||
|
),
|
||||||
body: loading
|
body: loading
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: installments.isEmpty
|
: installments.isEmpty
|
||||||
? const Center(
|
? _buildEmptyState()
|
||||||
child: Text("Installments not created yet",
|
|
||||||
style: TextStyle(fontSize: 18)),
|
|
||||||
)
|
|
||||||
: ListView.builder(
|
: ListView.builder(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: width * 0.04,
|
||||||
|
vertical: 16,
|
||||||
|
),
|
||||||
itemCount: installments.length,
|
itemCount: installments.length,
|
||||||
itemBuilder: (_, i) {
|
itemBuilder: (_, i) {
|
||||||
final inst = installments[i];
|
return InstallmentCard(inst: installments[i]);
|
||||||
return Card(
|
},
|
||||||
child: ListTile(
|
),
|
||||||
title: Text(
|
);
|
||||||
"Amount: ₹${inst['amount']?.toString() ?? '0'}"),
|
}
|
||||||
subtitle: Column(
|
|
||||||
|
Widget _buildEmptyState() {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.receipt_long,
|
||||||
|
size: 70, color: Colors.grey.shade400),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
"No Installments Created",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InstallmentCard extends StatelessWidget {
|
||||||
|
final Map inst;
|
||||||
|
|
||||||
|
const InstallmentCard({super.key, required this.inst});
|
||||||
|
|
||||||
|
String getString(key) => inst[key]?.toString() ?? "N/A";
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final width = MediaQuery.of(context).size.width;
|
||||||
|
final isTablet = width > 600;
|
||||||
|
|
||||||
|
final padding = isTablet ? 28.0 : 20.0;
|
||||||
|
final amountSize = isTablet ? 30.0 : 26.0;
|
||||||
|
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 18),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
blurRadius: 12,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(padding),
|
||||||
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Amount + Payment Method Row
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"Date: ${inst['installment_date'] ?? 'N/A'}"),
|
"₹${getString('amount')}",
|
||||||
Text(
|
style: TextStyle(
|
||||||
"Payment: ${inst['payment_method'] ?? 'N/A'}"),
|
fontSize: amountSize,
|
||||||
Text(
|
fontWeight: FontWeight.bold,
|
||||||
"Reference: ${inst['reference_no'] ?? 'N/A'}"),
|
letterSpacing: 0.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Payment Chip
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
vertical: isTablet ? 8 : 6,
|
||||||
|
horizontal: isTablet ? 16 : 12,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(50),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
getString('payment_method'),
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.blue.shade700,
|
||||||
|
fontSize: isTablet ? 15 : 13.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: isTablet ? 24 : 18),
|
||||||
|
|
||||||
|
// Responsive Info Rows
|
||||||
|
buildInfoRow(
|
||||||
|
Icons.calendar_month,
|
||||||
|
"Date",
|
||||||
|
getString("installment_date"),
|
||||||
|
isTablet
|
||||||
|
),
|
||||||
|
SizedBox(height: isTablet ? 14 : 10),
|
||||||
|
buildInfoRow(
|
||||||
|
Icons.confirmation_number,
|
||||||
|
"Reference",
|
||||||
|
getString("reference_no"),
|
||||||
|
isTablet
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: isTablet ? 24 : 18),
|
||||||
|
Divider(color: Colors.grey.shade300, thickness: 1),
|
||||||
|
SizedBox(height: isTablet ? 10 : 6),
|
||||||
|
|
||||||
|
// Align(
|
||||||
|
// alignment: Alignment.centerRight,
|
||||||
|
// child: Text(
|
||||||
|
// "Installment #${inst['id'] ?? ''}",
|
||||||
|
// style: TextStyle(
|
||||||
|
// fontSize: isTablet ? 15 : 13,
|
||||||
|
// color: Colors.grey.shade600,
|
||||||
|
// fontWeight: FontWeight.w500,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Responsive info row builder
|
||||||
|
Widget buildInfoRow(IconData icon, String label, String value, bool isTablet) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: isTablet ? 24 : 20, color: Colors.grey.shade600),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Text(
|
||||||
|
"$label:",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: isTablet ? 17 : 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.grey.shade700,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: isTablet ? 16 : 15,
|
||||||
|
color: Colors.grey.shade800,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
145
lib/screens/chat_file_viewer.dart
Normal file
145
lib/screens/chat_file_viewer.dart
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:photo_view/photo_view.dart';
|
||||||
|
import 'package:video_player/video_player.dart';
|
||||||
|
import 'package:chewie/chewie.dart';
|
||||||
|
import 'package:flutter_pdfview/flutter_pdfview.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
|
class ChatFileViewer {
|
||||||
|
/// Entry point used by chat screen
|
||||||
|
static void open(
|
||||||
|
BuildContext context, {
|
||||||
|
required String url,
|
||||||
|
required String fileType,
|
||||||
|
}) {
|
||||||
|
if (fileType.startsWith('image/')) {
|
||||||
|
_openImage(context, url);
|
||||||
|
} else if (fileType.startsWith('video/')) {
|
||||||
|
_openVideo(context, url);
|
||||||
|
} else if (fileType == 'application/pdf') {
|
||||||
|
_openPdf(context, url);
|
||||||
|
} else {
|
||||||
|
_downloadFile(context, url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================
|
||||||
|
// IMAGE VIEWER
|
||||||
|
// ===========================
|
||||||
|
static void _openImage(BuildContext context, String url) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => Scaffold(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
appBar: AppBar(backgroundColor: Colors.black),
|
||||||
|
body: PhotoView(
|
||||||
|
imageProvider: NetworkImage(url),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================
|
||||||
|
// VIDEO VIEWER
|
||||||
|
// ===========================
|
||||||
|
static void _openVideo(BuildContext context, String url) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => _VideoPlayerPage(url: url),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================
|
||||||
|
// PDF VIEWER
|
||||||
|
// ===========================
|
||||||
|
static Future<void> _openPdf(
|
||||||
|
BuildContext context, String url) async {
|
||||||
|
final dir = await getTemporaryDirectory();
|
||||||
|
final path = "${dir.path}/chat.pdf";
|
||||||
|
|
||||||
|
await Dio().download(url, path);
|
||||||
|
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => Scaffold(
|
||||||
|
appBar: AppBar(title: const Text("PDF")),
|
||||||
|
body: PDFView(filePath: path),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================
|
||||||
|
// FILE DOWNLOAD
|
||||||
|
// ===========================
|
||||||
|
static Future<void> _downloadFile(
|
||||||
|
BuildContext context, String url) async {
|
||||||
|
final dir = await getExternalStorageDirectory();
|
||||||
|
final fileName = url.split('/').last;
|
||||||
|
final path = "${dir!.path}/$fileName";
|
||||||
|
|
||||||
|
await Dio().download(url, path);
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("File downloaded: $fileName")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================
|
||||||
|
// VIDEO PLAYER WIDGET
|
||||||
|
// ===========================
|
||||||
|
class _VideoPlayerPage extends StatefulWidget {
|
||||||
|
final String url;
|
||||||
|
const _VideoPlayerPage({required this.url});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_VideoPlayerPage> createState() => _VideoPlayerPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VideoPlayerPageState extends State<_VideoPlayerPage> {
|
||||||
|
late VideoPlayerController _videoController;
|
||||||
|
ChewieController? _chewieController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_videoController = VideoPlayerController.network(widget.url)
|
||||||
|
..initialize().then((_) {
|
||||||
|
setState(() {
|
||||||
|
_chewieController = ChewieController(
|
||||||
|
videoPlayerController: _videoController,
|
||||||
|
autoPlay: true,
|
||||||
|
looping: false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_videoController.dispose();
|
||||||
|
_chewieController?.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text("Video")),
|
||||||
|
body: Center(
|
||||||
|
child: _chewieController == null
|
||||||
|
? const CircularProgressIndicator()
|
||||||
|
: Chewie(controller: _chewieController!),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,386 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class ChatScreen extends StatelessWidget {
|
import '../services/chat_service.dart';
|
||||||
|
import '../services/reverb_socket_service.dart';
|
||||||
|
import '../services/dio_client.dart';
|
||||||
|
import '../providers/chat_unread_provider.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:file_selector/file_selector.dart';
|
||||||
|
import 'chat_file_viewer.dart';
|
||||||
|
import '../widgets/chat_file_preview.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class ChatScreen extends StatefulWidget {
|
||||||
const ChatScreen({super.key});
|
const ChatScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ChatScreen> createState() => _ChatScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatScreenState extends State<ChatScreen> {
|
||||||
|
final TextEditingController _messageCtrl = TextEditingController();
|
||||||
|
final ScrollController _scrollCtrl = ScrollController();
|
||||||
|
Map<String, dynamic>? uploadingMessage;
|
||||||
|
|
||||||
|
late ChatService _chatService;
|
||||||
|
final ReverbSocketService _socket = ReverbSocketService();
|
||||||
|
|
||||||
|
int? ticketId;
|
||||||
|
List<Map<String, dynamic>> messages = [];
|
||||||
|
bool isLoading = true;
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// INIT STATE
|
||||||
|
// ============================
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_chatService = ChatService(DioClient.getInstance(context));
|
||||||
|
|
||||||
|
// 🔔 Mark chat as OPEN (important)
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
context.read<ChatUnreadProvider>().setChatOpen(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
_initChat();
|
||||||
|
}
|
||||||
|
String _guessMimeType(String path) {
|
||||||
|
final lower = path.toLowerCase();
|
||||||
|
if (lower.endsWith('.jpg') || lower.endsWith('.png')) return 'image/*';
|
||||||
|
if (lower.endsWith('.mp4')) return 'video/*';
|
||||||
|
if (lower.endsWith('.pdf')) return 'application/pdf';
|
||||||
|
return 'application/octet-stream';
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickAndSendFile() async {
|
||||||
|
final XFile? picked = await openFile();
|
||||||
|
if (picked == null || ticketId == null) return;
|
||||||
|
|
||||||
|
final file = File(picked.path);
|
||||||
|
|
||||||
|
// 1️⃣ Show uploading UI
|
||||||
|
setState(() {
|
||||||
|
uploadingMessage = {
|
||||||
|
'local_file': file,
|
||||||
|
'file_type': _guessMimeType(file.path),
|
||||||
|
'progress': 0.0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2️⃣ Upload (NO adding message)
|
||||||
|
await _chatService.sendFile(
|
||||||
|
ticketId!,
|
||||||
|
file,
|
||||||
|
onProgress: (progress) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
uploadingMessage!['progress'] = progress;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3️⃣ Remove sending bubble ONLY
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
uploadingMessage = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🚫 DO NOT add message here
|
||||||
|
// WebSocket will handle it
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// INIT CHAT
|
||||||
|
// ============================
|
||||||
|
Future<void> _initChat() async {
|
||||||
|
// 1️⃣ Start chat
|
||||||
|
final ticketRes = await _chatService.startChat();
|
||||||
|
ticketId = ticketRes['ticket']['id'];
|
||||||
|
|
||||||
|
// 2️⃣ Load messages
|
||||||
|
final msgs = await _chatService.getMessages(ticketId!);
|
||||||
|
messages = List<Map<String, dynamic>>.from(msgs);
|
||||||
|
|
||||||
|
// 3️⃣ Realtime socket
|
||||||
|
await _socket.connect(
|
||||||
|
context: context,
|
||||||
|
ticketId: ticketId!,
|
||||||
|
onMessage: (msg) {
|
||||||
|
final incomingClientId = msg['client_id'];
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
// 🧹 Remove local temp message with same client_id
|
||||||
|
messages.removeWhere(
|
||||||
|
(m) => m['client_id'] != null &&
|
||||||
|
m['client_id'] == incomingClientId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ✅ Add confirmed socket message
|
||||||
|
messages.add(msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
_scrollToBottom();
|
||||||
|
},
|
||||||
|
|
||||||
|
onAdminMessage: () {
|
||||||
|
if (!mounted) {
|
||||||
|
context.read<ChatUnreadProvider>().increment();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => isLoading = false);
|
||||||
|
_scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// SCROLL
|
||||||
|
// ============================
|
||||||
|
void _scrollToBottom() {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (_scrollCtrl.hasClients) {
|
||||||
|
_scrollCtrl.jumpTo(_scrollCtrl.position.maxScrollExtent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// SEND MESSAGE
|
||||||
|
// ============================
|
||||||
|
Future<void> _sendMessage() async {
|
||||||
|
final text = _messageCtrl.text.trim();
|
||||||
|
if (text.isEmpty || ticketId == null) return;
|
||||||
|
|
||||||
|
_messageCtrl.clear();
|
||||||
|
|
||||||
|
final clientId = DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
|
|
||||||
|
// 1️⃣ ADD LOCAL MESSAGE IMMEDIATELY
|
||||||
|
setState(() {
|
||||||
|
messages.add({
|
||||||
|
'client_id': clientId,
|
||||||
|
'sender_type': 'App\\Models\\User',
|
||||||
|
'message': text,
|
||||||
|
'file_path': null,
|
||||||
|
'file_type': null,
|
||||||
|
'sending': true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
_scrollToBottom();
|
||||||
|
|
||||||
|
// 2️⃣ SEND TO SERVER
|
||||||
|
await _chatService.sendMessage(
|
||||||
|
ticketId!,
|
||||||
|
message: text,
|
||||||
|
clientId: clientId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// DISPOSE
|
||||||
|
// ============================
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
// 🔕 Mark chat CLOSED
|
||||||
|
context.read<ChatUnreadProvider>().setChatOpen(false);
|
||||||
|
|
||||||
|
_socket.disconnect();
|
||||||
|
_messageCtrl.dispose();
|
||||||
|
_scrollCtrl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// UI
|
||||||
|
// ============================
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Center(
|
return Scaffold(
|
||||||
child: Text("Invoice Content He", style: TextStyle(fontSize: 18)),
|
appBar: AppBar(title: const Text("Support Chat")),
|
||||||
|
body: isLoading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildMessages()),
|
||||||
|
_buildInput(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMessageContent({
|
||||||
|
String? message,
|
||||||
|
String? filePath,
|
||||||
|
String? fileType,
|
||||||
|
required bool isUser,
|
||||||
|
}) {
|
||||||
|
final textColor = isUser ? Colors.white : Colors.black;
|
||||||
|
|
||||||
|
if (filePath == null) {
|
||||||
|
return Text(message ?? '', style: TextStyle(color: textColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
final url = "${DioClient.baseUrl}/storage/$filePath";
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
ChatFileViewer.open(
|
||||||
|
context,
|
||||||
|
url: url,
|
||||||
|
fileType: fileType ?? '',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(_iconForFile(fileType), color: textColor),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
_labelForFile(fileType),
|
||||||
|
style: TextStyle(color: textColor),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData _iconForFile(String? type) {
|
||||||
|
if (type == null) return Icons.insert_drive_file;
|
||||||
|
if (type.startsWith('image/')) return Icons.image;
|
||||||
|
if (type.startsWith('video/')) return Icons.play_circle_fill;
|
||||||
|
if (type == 'application/pdf') return Icons.picture_as_pdf;
|
||||||
|
return Icons.insert_drive_file;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _labelForFile(String? type) {
|
||||||
|
if (type == null) return "File";
|
||||||
|
if (type.startsWith('image/')) return "Image";
|
||||||
|
if (type.startsWith('video/')) return "Video";
|
||||||
|
if (type == 'application/pdf') return "PDF";
|
||||||
|
return "Download file";
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMessages() {
|
||||||
|
return ListView(
|
||||||
|
controller: _scrollCtrl,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
children: [
|
||||||
|
// EXISTING MESSAGES
|
||||||
|
...messages.map((msg) {
|
||||||
|
final isUser = msg['sender_type'] == 'App\\Models\\User';
|
||||||
|
|
||||||
|
return Align(
|
||||||
|
alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 6),
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isUser ? Colors.blue : Colors.grey.shade300,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: msg['file_path'] == null
|
||||||
|
? Text(
|
||||||
|
msg['message'] ?? '',
|
||||||
|
style: TextStyle(
|
||||||
|
color: isUser ? Colors.white : Colors.black,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ChatFilePreview(
|
||||||
|
filePath: msg['file_path'],
|
||||||
|
fileType: msg['file_type'] ?? '',
|
||||||
|
isUser: isUser,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
|
||||||
|
// ⏳ UPLOADING MESSAGE
|
||||||
|
// ⏳ UPLOADING MESSAGE (SAFE & NO DUPLICATE)
|
||||||
|
if (uploadingMessage != null)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 6),
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// ✅ SHOW PREVIEW ONLY FOR IMAGE / VIDEO
|
||||||
|
if (uploadingMessage!['file_type'].startsWith('image/') ||
|
||||||
|
uploadingMessage!['file_type'].startsWith('video/'))
|
||||||
|
ChatFilePreview(
|
||||||
|
filePath: uploadingMessage!['local_file'].path,
|
||||||
|
fileType: uploadingMessage!['file_type'],
|
||||||
|
isUser: true,
|
||||||
|
isLocal: true,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
// 📄 DOCUMENT PLACEHOLDER
|
||||||
|
const Icon(
|
||||||
|
Icons.insert_drive_file,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 40,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
LinearProgressIndicator(
|
||||||
|
value: uploadingMessage!['progress'],
|
||||||
|
backgroundColor: Colors.white24,
|
||||||
|
valueColor:
|
||||||
|
const AlwaysStoppedAnimation<Color>(Colors.white),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
|
||||||
|
const Text(
|
||||||
|
"Sending…",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInput() {
|
||||||
|
return SafeArea(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.attach_file),
|
||||||
|
onPressed: _pickAndSendFile,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _messageCtrl,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: "Type message",
|
||||||
|
border: InputBorder.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.send),
|
||||||
|
onPressed: _sendMessage,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,250 +12,586 @@ class DashboardScreen extends StatefulWidget {
|
|||||||
State<DashboardScreen> createState() => _DashboardScreenState();
|
State<DashboardScreen> createState() => _DashboardScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DashboardScreenState extends State<DashboardScreen> {
|
class _DashboardScreenState extends State<DashboardScreen>
|
||||||
|
with TickerProviderStateMixin {
|
||||||
|
late AnimationController _scaleCtrl;
|
||||||
|
late AnimationController _shineCtrl;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
_scaleCtrl = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 1200),
|
||||||
|
lowerBound: 0.97,
|
||||||
|
upperBound: 1.0,
|
||||||
|
)..repeat(reverse: true);
|
||||||
|
|
||||||
|
_shineCtrl = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
)..repeat();
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
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);
|
final dash = Provider.of<DashboardProvider>(context, listen: false);
|
||||||
dash.init(context);
|
dash.init(context);
|
||||||
await dash.loadSummary(context);
|
await dash.loadSummary(context);
|
||||||
|
|
||||||
// STEP 3: Load marks AFTER refresh
|
|
||||||
final marks = Provider.of<MarkListProvider>(context, listen: false);
|
final marks = Provider.of<MarkListProvider>(context, listen: false);
|
||||||
marks.init(context);
|
marks.init(context);
|
||||||
await marks.loadMarks(context);
|
await marks.loadMarks(context);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scaleCtrl.dispose();
|
||||||
|
_shineCtrl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
void _showAddMarkForm() {
|
// ============================================================
|
||||||
|
// CENTERED ADD MARK POPUP
|
||||||
|
// ============================================================
|
||||||
|
void _showAddMarkForm(double scale) {
|
||||||
final markCtrl = TextEditingController();
|
final markCtrl = TextEditingController();
|
||||||
final originCtrl = TextEditingController();
|
final originCtrl = TextEditingController();
|
||||||
final destCtrl = TextEditingController();
|
final destCtrl = TextEditingController();
|
||||||
|
|
||||||
showModalBottomSheet(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
barrierDismissible: true,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
|
|
||||||
builder: (_) {
|
builder: (_) {
|
||||||
return Padding(
|
return Center(
|
||||||
padding: EdgeInsets.fromLTRB(
|
child: Material(
|
||||||
18,
|
color: Colors.transparent,
|
||||||
18,
|
child: Container(
|
||||||
18,
|
width: MediaQuery.of(context).size.width * 0.88,
|
||||||
MediaQuery.of(context).viewInsets.bottom + 20,
|
padding: EdgeInsets.all(20 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20 * scale),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.2),
|
||||||
|
blurRadius: 16,
|
||||||
|
offset: const Offset(0, 6),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
"Add Mark No",
|
"Add Mark No",
|
||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
fontSize: 20 * scale,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.indigo,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
TextField(controller: markCtrl, decoration: const InputDecoration(labelText: "Mark No")),
|
SizedBox(height: 18 * scale),
|
||||||
const SizedBox(height: 12),
|
|
||||||
|
|
||||||
TextField(controller: originCtrl, decoration: const InputDecoration(labelText: "Origin")),
|
_inputField(markCtrl, "Mark No", scale),
|
||||||
const SizedBox(height: 12),
|
SizedBox(height: 12 * scale),
|
||||||
|
_inputField(originCtrl, "Origin", scale),
|
||||||
|
SizedBox(height: 12 * scale),
|
||||||
|
_inputField(destCtrl, "Destination", scale),
|
||||||
|
|
||||||
TextField(controller: destCtrl, decoration: const InputDecoration(labelText: "Destination")),
|
SizedBox(height: 22 * scale),
|
||||||
const SizedBox(height: 20),
|
|
||||||
|
|
||||||
ElevatedButton(
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: [Colors.indigo, Colors.deepPurple],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(12 * scale),
|
||||||
|
),
|
||||||
|
child: ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final mark = markCtrl.text.trim();
|
final mark = markCtrl.text.trim();
|
||||||
final origin = originCtrl.text.trim();
|
final origin = originCtrl.text.trim();
|
||||||
final dest = destCtrl.text.trim();
|
final dest = destCtrl.text.trim();
|
||||||
|
|
||||||
if (mark.isEmpty || origin.isEmpty || dest.isEmpty) {
|
if (mark.isEmpty || origin.isEmpty || dest.isEmpty) {
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
.showSnackBar(const SnackBar(content: Text("All fields required")));
|
const SnackBar(content: Text("All fields are required")),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final provider = Provider.of<MarkListProvider>(context, listen: false);
|
final provider =
|
||||||
final res = await provider.addMark(context, mark, origin, dest);
|
Provider.of<MarkListProvider>(context, listen: false);
|
||||||
|
final res =
|
||||||
|
await provider.addMark(context, mark, origin, dest);
|
||||||
|
|
||||||
if (res['success'] == true) {
|
if (res['success'] == true) {
|
||||||
|
await Provider.of<MarkListProvider>(context,
|
||||||
|
listen: false)
|
||||||
|
.loadMarks(context);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
} else {
|
} else {
|
||||||
final msg = res['message'] ?? "Failed";
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
SnackBar(content: Text(res['message'] ?? "Failed")),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text("Submit"),
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
shadowColor: Colors.transparent,
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 14 * scale),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12 * scale),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
"Submit",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16 * scale,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 6 * scale),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// MAIN UI (Responsive)
|
||||||
|
// ============================================================
|
||||||
@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 dash = Provider.of<DashboardProvider>(context);
|
||||||
final marks = Provider.of<MarkListProvider>(context);
|
final marks = Provider.of<MarkListProvider>(context);
|
||||||
|
|
||||||
final name = auth.user?['customer_name'] ?? 'User';
|
final name = auth.user?['customer_name'] ?? "User";
|
||||||
|
final width = MediaQuery.of(context).size.width;
|
||||||
if (dash.loading) {
|
final scale = (width / 430).clamp(0.88, 1.08);
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(18),
|
padding: EdgeInsets.all(16 * scale),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// HEADER
|
// WELCOME CARD WITH ANIMATION
|
||||||
Text(
|
AnimatedBuilder(
|
||||||
"Welcome, $name 👋",
|
animation: _scaleCtrl,
|
||||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600),
|
builder: (_, __) {
|
||||||
),
|
return Transform.scale(
|
||||||
const SizedBox(height: 20),
|
scale: _scaleCtrl.value,
|
||||||
|
child: Stack(
|
||||||
// ORDER SUMMARY
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
_statBox("Active", dash.activeOrders, Colors.blue),
|
Container(
|
||||||
_statBox("In Transit", dash.inTransitOrders, Colors.orange),
|
padding: EdgeInsets.all(20 * scale),
|
||||||
_statBox("Delivered", dash.deliveredOrders, Colors.green),
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: [Color(0xFFFFA726), Color(0xFFFFEB3B)],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(18 * scale),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
"Welcome, $name 👋",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24 * scale,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Shine Animation
|
||||||
|
AnimatedBuilder(
|
||||||
|
animation: _shineCtrl,
|
||||||
|
builder: (_, __) {
|
||||||
|
final left = _shineCtrl.value *
|
||||||
|
(MediaQuery.of(context).size.width + 140) -
|
||||||
|
140;
|
||||||
|
|
||||||
|
return Positioned(
|
||||||
|
left: left,
|
||||||
|
top: -40 * scale,
|
||||||
|
bottom: -40 * scale,
|
||||||
|
child: Transform.rotate(
|
||||||
|
angle: -0.45,
|
||||||
|
child: Container(
|
||||||
|
width: 120 * scale,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Colors.white.withOpacity(0),
|
||||||
|
Colors.white.withOpacity(.3),
|
||||||
|
Colors.white.withOpacity(0),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
),
|
||||||
_valueCard("Total Value", dash.totalValue),
|
),
|
||||||
const SizedBox(height: 10),
|
),
|
||||||
_valueCard("Raw Amount", "₹${dash.totalRaw.toStringAsFixed(2)}"),
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: 30),
|
SizedBox(height: 18 * scale),
|
||||||
|
|
||||||
|
_summarySection(
|
||||||
|
dash,
|
||||||
|
rawAmount: "₹${dash.totalRaw ?? 0}",
|
||||||
|
scale: scale,
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 26 * scale),
|
||||||
|
|
||||||
|
// MARK LIST CARD
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(16 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(14 * scale),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(.06),
|
||||||
|
blurRadius: 8 * scale,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Add Mark Button
|
||||||
|
Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: width * 0.95,
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: [Colors.indigo, Colors.deepPurple],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(12 * scale),
|
||||||
|
),
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => _showAddMarkForm(scale),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
shadowColor: Colors.transparent,
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 14 * scale),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
"Add Mark No",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16 * scale,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 16 * scale),
|
||||||
|
|
||||||
// ADD + VIEW ALL BUTTONS SIDE BY SIDE
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
ElevatedButton.icon(
|
Text(
|
||||||
icon: const Icon(Icons.add),
|
"Latest Mark Numbers",
|
||||||
label: const Text("Add Mark No"),
|
style: TextStyle(
|
||||||
onPressed: _showAddMarkForm,
|
fontSize: 18 * scale,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
if (marks.marks.length > 0)
|
if (marks.marks.isNotEmpty)
|
||||||
TextButton(
|
GestureDetector(
|
||||||
onPressed: () {
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (_) => const MarkListScreen()),
|
MaterialPageRoute(
|
||||||
|
builder: (_) => const MarkListScreen()),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: Text(
|
||||||
"View All →",
|
"View All →",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 15 * scale,
|
||||||
color: Colors.indigo,
|
color: Colors.indigo,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 12 * scale),
|
||||||
|
|
||||||
|
// Scrollable Mark List
|
||||||
|
SizedBox(
|
||||||
|
height: 300 * scale,
|
||||||
|
child: Scrollbar(
|
||||||
|
thumbVisibility: true,
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
itemCount: marks.marks.length,
|
||||||
|
itemBuilder: (context, i) =>
|
||||||
|
_markTile(marks.marks[i], scale),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 40 * scale),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// SUMMARY SECTION
|
||||||
|
// ============================================================
|
||||||
|
Widget _summarySection(dash,
|
||||||
|
{required String rawAmount, required double scale}) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(14 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(14 * scale),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(.06),
|
||||||
|
blurRadius: 8 * scale,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
_summaryTile("Active Orders", dash.activeOrders ?? 0,
|
||||||
|
Colors.blue, Icons.inventory, scale),
|
||||||
|
_summaryTile("In Transit", dash.inTransitOrders ?? 0,
|
||||||
|
Colors.orange, Icons.local_shipping, scale),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 12 * scale),
|
||||||
|
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
_summaryTile("Delivered", dash.deliveredOrders ?? 0,
|
||||||
|
Colors.green, Icons.check_circle, scale),
|
||||||
|
_summaryTile("Total Value", "₹${dash.totalValue ?? 0}",
|
||||||
|
Colors.teal, Icons.money, scale),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 16 * scale),
|
||||||
|
|
||||||
|
_rawAmountTile("Raw Amount", rawAmount, scale),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _summaryTile(String title, dynamic value, Color color,
|
||||||
|
IconData icon, double scale) {
|
||||||
|
return Container(
|
||||||
|
width: MediaQuery.of(context).size.width * 0.41,
|
||||||
|
padding: EdgeInsets.all(14 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withOpacity(.12),
|
||||||
|
borderRadius: BorderRadius.circular(14 * scale),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
value.toString(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20 * scale,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 2 * scale),
|
||||||
|
|
||||||
|
SizedBox(
|
||||||
|
width: 100 * scale,
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13 * scale,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
Icon(icon, size: 28 * scale, color: color),
|
||||||
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 _rawAmountTile(String title, String value, double scale) {
|
||||||
Widget _statBox(String title, int value, Color color) {
|
|
||||||
return Container(
|
return Container(
|
||||||
width: 110,
|
padding: EdgeInsets.all(14 * scale),
|
||||||
padding: const EdgeInsets.all(14),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: color.withOpacity(0.15),
|
color: Colors.deepPurple.shade50,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(14 * scale),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(value.toString(),
|
Column(
|
||||||
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(title, style: const TextStyle(fontSize: 14)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _valueCard(String title, String value) {
|
|
||||||
return Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.all(18),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.indigo.shade50,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(title,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 14, fontWeight: FontWeight.w600, color: Colors.grey)),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
Text(
|
Text(
|
||||||
value,
|
value,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 20 * scale,
|
||||||
|
color: Colors.deepPurple,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.indigo,
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13 * scale,
|
||||||
|
color: Colors.grey,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
Icon(Icons.currency_rupee,
|
||||||
|
size: 28 * scale, color: Colors.deepPurple),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// MARK TILE
|
||||||
|
// ============================================================
|
||||||
|
Widget _markTile(dynamic m, double scale) {
|
||||||
|
return Container(
|
||||||
|
margin: EdgeInsets.only(bottom: 12 * scale),
|
||||||
|
padding: EdgeInsets.all(14 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: [Color(0xFF42A5F5), Color(0xFF80DEEA)],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(14 * scale),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
m['mark_no'],
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18 * scale,
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"${m['origin']} → ${m['destination']}",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14 * scale,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 10 * scale,
|
||||||
|
vertical: 6 * scale,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: (m['status'] ?? '')
|
||||||
|
.toString()
|
||||||
|
.toLowerCase() ==
|
||||||
|
'active'
|
||||||
|
? const LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Color(0xFF2ECC71), // Green
|
||||||
|
Color(0xFF16A085), // Teal Green
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: const LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Color(0xFFE74C3C), // Red
|
||||||
|
Color(0xFFC0392B), // Dark Red
|
||||||
|
],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8 * scale),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
m['status'],
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12 * scale,
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// INPUT FIELD
|
||||||
|
// ============================================================
|
||||||
|
Widget _inputField(TextEditingController controller, String label, double scale) {
|
||||||
|
return TextField(
|
||||||
|
controller: controller,
|
||||||
|
style: TextStyle(fontSize: 15 * scale),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: label,
|
||||||
|
labelStyle: TextStyle(fontSize: 14 * scale),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.lightBlue.shade50,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12 * scale),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
contentPadding:
|
||||||
|
EdgeInsets.symmetric(horizontal: 14 * scale, vertical: 14 * scale),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final profile = Provider.of<UserProfileProvider>(
|
final profile =
|
||||||
context, listen: false).profile;
|
Provider.of<UserProfileProvider>(context, listen: false).profile;
|
||||||
|
|
||||||
nameCtrl.text = profile?.customerName ?? '';
|
nameCtrl.text = profile?.customerName ?? '';
|
||||||
companyCtrl.text = profile?.companyName ?? '';
|
companyCtrl.text = profile?.companyName ?? '';
|
||||||
@@ -32,8 +32,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _submit() async {
|
Future<void> _submit() async {
|
||||||
final provider =
|
final provider = Provider.of<UserProfileProvider>(context, listen: false);
|
||||||
Provider.of<UserProfileProvider>(context, listen: false);
|
|
||||||
|
|
||||||
final data = {
|
final data = {
|
||||||
"customer_name": nameCtrl.text,
|
"customer_name": nameCtrl.text,
|
||||||
@@ -44,14 +43,16 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
|||||||
"pincode": pincodeCtrl.text,
|
"pincode": pincodeCtrl.text,
|
||||||
};
|
};
|
||||||
|
|
||||||
final success =
|
final success = await provider.sendProfileUpdateRequest(context, data);
|
||||||
await provider.sendProfileUpdateRequest(context, data);
|
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(success
|
content: Text(
|
||||||
|
success
|
||||||
? "Request submitted. Wait for admin approval."
|
? "Request submitted. Wait for admin approval."
|
||||||
: "Failed to submit request")),
|
: "Failed to submit request",
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (success) Navigator.pop(context);
|
if (success) Navigator.pop(context);
|
||||||
@@ -59,38 +60,179 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
final darkBlue = const Color(0xFF003B73);
|
||||||
appBar: AppBar(title: const Text("Edit Profile")),
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
body: Padding(
|
|
||||||
padding: const EdgeInsets.all(18),
|
return Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [Color(0xFFB3E5FC), Color(0xFFE1F5FE)],
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
body: SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
double scale = (screenWidth / 390).clamp(0.75, 1.3);
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 18 * scale),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(height: 8 * scale),
|
||||||
|
|
||||||
|
/// BACK BUTTON
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.arrow_back_ios_new, size: 22 * scale),
|
||||||
|
color: Colors.red,
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 10 * scale),
|
||||||
|
|
||||||
|
/// TITLE
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
"Edit Profile",
|
||||||
|
style: TextStyle(
|
||||||
|
color: darkBlue,
|
||||||
|
fontSize: 26 * scale,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 20 * scale),
|
||||||
|
|
||||||
|
/// RESPONSIVE CENTERED FORM CARD
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
width: screenWidth > 650 ? 550 : double.infinity,
|
||||||
|
padding: EdgeInsets.all(24 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.95),
|
||||||
|
borderRadius: BorderRadius.circular(20 * scale),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black12.withOpacity(0.15),
|
||||||
|
blurRadius: 18 * scale,
|
||||||
|
offset: Offset(0, 6 * scale),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_field("Name", nameCtrl),
|
_buildField("Full Name", nameCtrl, scale),
|
||||||
_field("Company", companyCtrl),
|
_buildField("Company Name", companyCtrl, scale),
|
||||||
_field("Email", emailCtrl),
|
_buildField("Email Address", emailCtrl, scale),
|
||||||
_field("Mobile", mobileCtrl),
|
_buildField("Mobile Number", mobileCtrl, scale),
|
||||||
_field("Address", addressCtrl),
|
_buildField("Address", addressCtrl, scale),
|
||||||
_field("Pincode", pincodeCtrl),
|
_buildField("Pincode", pincodeCtrl, scale),
|
||||||
|
|
||||||
const SizedBox(height: 20),
|
SizedBox(height: 25 * scale),
|
||||||
ElevatedButton(
|
|
||||||
|
/// RESPONSIVE GRADIENT SUBMIT BUTTON
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 50 * scale,
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(14 * scale),
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Color(0xFF0052D4),
|
||||||
|
Color(0xFF4364F7),
|
||||||
|
Color(0xFF6FB1FC),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.blueAccent.withOpacity(0.4),
|
||||||
|
blurRadius: 10 * scale,
|
||||||
|
offset: Offset(0, 4 * scale),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ElevatedButton(
|
||||||
onPressed: _submit,
|
onPressed: _submit,
|
||||||
child: const Text("Submit Update Request"),
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
shadowColor: Colors.transparent,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(14 * scale),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
"Submit Update Request",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 17 * scale,
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 30 * scale),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _field(String title, TextEditingController ctrl) {
|
/// Reusable Responsive TextField Builder
|
||||||
|
Widget _buildField(
|
||||||
|
String label, TextEditingController ctrl, double scale) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 14),
|
padding: EdgeInsets.only(bottom: 18 * scale),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(16 * scale),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black12.withOpacity(0.06),
|
||||||
|
blurRadius: 10 * scale,
|
||||||
|
offset: Offset(0, 3 * scale),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: ctrl,
|
controller: ctrl,
|
||||||
|
style: TextStyle(fontSize: 15 * scale),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: title,
|
filled: true,
|
||||||
border: OutlineInputBorder(),
|
fillColor: Colors.white,
|
||||||
|
labelText: label,
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
color: const Color(0xFF003B73),
|
||||||
|
fontSize: 14 * scale,
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16 * scale),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16 * scale),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: const Color(0xFF003B73),
|
||||||
|
width: 2 * scale,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import '../config/api_config.dart';
|
||||||
import '../services/dio_client.dart';
|
import '../services/dio_client.dart';
|
||||||
import '../services/invoice_service.dart';
|
import '../services/invoice_service.dart';
|
||||||
|
import '../widgets/invoice_detail_view.dart';
|
||||||
|
|
||||||
class InvoiceDetailScreen extends StatefulWidget {
|
class InvoiceDetailScreen extends StatefulWidget {
|
||||||
final int invoiceId;
|
final int invoiceId;
|
||||||
@@ -22,147 +31,147 @@ class _InvoiceDetailScreenState extends State<InvoiceDetailScreen> {
|
|||||||
|
|
||||||
Future<void> load() async {
|
Future<void> load() async {
|
||||||
final service = InvoiceService(DioClient.getInstance(context));
|
final service = InvoiceService(DioClient.getInstance(context));
|
||||||
|
try {
|
||||||
final res = await service.getInvoiceDetails(widget.invoiceId);
|
final res = await service.getInvoiceDetails(widget.invoiceId);
|
||||||
|
|
||||||
if (res['success'] == true) {
|
if (res['success'] == true) {
|
||||||
invoice = res['invoice'] ?? {};
|
invoice = res['invoice'] ?? {};
|
||||||
|
} else {
|
||||||
|
invoice = {};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// handle error gracefully
|
||||||
|
invoice = {};
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => loading = false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loading = false;
|
String? get pdfUrl {
|
||||||
setState(() {});
|
final path = invoice['pdf_path'];
|
||||||
|
if (path == null || path.toString().isEmpty) return null;
|
||||||
|
|
||||||
|
return ApiConfig.fileBaseUrl + path;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ---------- REUSABLE ROW ----------
|
static const MethodChannel _mediaScanner =
|
||||||
Widget row(String label, dynamic value) {
|
MethodChannel('media_scanner');
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
Future<void> _scanFile(String path) async {
|
||||||
child: Row(
|
try {
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
await _mediaScanner.invokeMethod('scanFile', {'path': path});
|
||||||
children: [
|
} catch (e) {
|
||||||
Text(label, style: const TextStyle(fontSize: 14, color: Colors.grey)),
|
debugPrint("❌ MediaScanner error: $e");
|
||||||
Expanded(
|
}
|
||||||
child: Text(
|
}
|
||||||
value?.toString().isNotEmpty == true ? value.toString() : "N/A",
|
|
||||||
textAlign: TextAlign.end,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 15,
|
Future<File?> _downloadPdf(String url) async {
|
||||||
fontWeight: FontWeight.w600,
|
try {
|
||||||
),
|
debugPrint("📥 PDF URL: $url");
|
||||||
),
|
|
||||||
),
|
final fileName = url.split('/').last;
|
||||||
],
|
|
||||||
),
|
// ✅ SAME FOLDER AS CHAT FILES
|
||||||
|
final baseDir = Directory('/storage/emulated/0/Download/KentChat');
|
||||||
|
|
||||||
|
if (!await baseDir.exists()) {
|
||||||
|
await baseDir.create(recursive: true);
|
||||||
|
debugPrint("📁 Created directory: ${baseDir.path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
final filePath = "${baseDir.path}/$fileName";
|
||||||
|
debugPrint("📄 Saving PDF to: $filePath");
|
||||||
|
|
||||||
|
await Dio().download(
|
||||||
|
url,
|
||||||
|
filePath,
|
||||||
|
onReceiveProgress: (received, total) {
|
||||||
|
if (total > 0) {
|
||||||
|
debugPrint(
|
||||||
|
"⬇ Downloading: ${(received / total * 100).toStringAsFixed(1)}%",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🔔 VERY IMPORTANT: Notify Android system (same as chat)
|
||||||
|
await _scanFile(filePath);
|
||||||
|
|
||||||
|
debugPrint("✅ PDF Downloaded & Scanned Successfully");
|
||||||
|
|
||||||
|
return File(filePath);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("❌ PDF download error: $e");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final width = MediaQuery.of(context).size.width;
|
||||||
|
|
||||||
|
// ⭐ COMPACT RESPONSIVE SCALE
|
||||||
|
final scale = (width / 430).clamp(0.88, 1.08);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text("Invoice Details")),
|
appBar: AppBar(
|
||||||
body: loading
|
title: const Text("Invoice Details"),
|
||||||
? const Center(child: CircularProgressIndicator())
|
actions: pdfUrl == null
|
||||||
|
? []
|
||||||
/// ================ INVOICE DATA ================
|
: [
|
||||||
: Padding(
|
// DOWNLOAD
|
||||||
padding: const EdgeInsets.all(16),
|
IconButton(
|
||||||
child: ListView(
|
icon: const Icon(Icons.download_rounded),
|
||||||
children: [
|
onPressed: () async {
|
||||||
/// -------- Invoice Summary --------
|
final file = await _downloadPdf(pdfUrl!);
|
||||||
const Text(
|
if (file != null && mounted) {
|
||||||
"Invoice Summary",
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
const SnackBar(content: Text("Invoice downloaded")),
|
||||||
),
|
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}),
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
// SHARE
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.share_rounded),
|
||||||
|
onPressed: () async {
|
||||||
|
final file = await _downloadPdf(pdfUrl!);
|
||||||
|
if (file != null) {
|
||||||
|
await Share.shareXFiles(
|
||||||
|
[XFile(file.path)],
|
||||||
|
text: "Invoice ${invoice['invoice_number']}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
body: loading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: invoice.isEmpty
|
||||||
|
? Center(
|
||||||
|
child: Text(
|
||||||
|
"No Invoice Data Found",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18 * scale,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Padding(
|
||||||
|
padding: EdgeInsets.all(12 * scale),
|
||||||
|
// NOTE: InvoiceDetailView should handle its own responsiveness.
|
||||||
|
// If you want it to follow the same `scale`, I can update that widget to accept `scale`.
|
||||||
|
child: InvoiceDetailView(invoice: invoice),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../providers/invoice_installment_screen.dart';
|
import '../providers/invoice_installment_screen.dart';
|
||||||
import '../providers/invoice_provider.dart';
|
import '../providers/invoice_provider.dart';
|
||||||
import '../services/dio_client.dart';
|
|
||||||
import '../services/invoice_service.dart';
|
|
||||||
import 'invoice_detail_screen.dart';
|
import 'invoice_detail_screen.dart';
|
||||||
|
|
||||||
|
|
||||||
class InvoiceScreen extends StatefulWidget {
|
class InvoiceScreen extends StatefulWidget {
|
||||||
const InvoiceScreen({super.key});
|
const InvoiceScreen({super.key});
|
||||||
|
|
||||||
@@ -15,10 +12,11 @@ class InvoiceScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _InvoiceScreenState extends State<InvoiceScreen> {
|
class _InvoiceScreenState extends State<InvoiceScreen> {
|
||||||
|
String searchQuery = "";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
Provider.of<InvoiceProvider>(context, listen: false)
|
Provider.of<InvoiceProvider>(context, listen: false)
|
||||||
.loadInvoices(context);
|
.loadInvoices(context);
|
||||||
@@ -29,78 +27,263 @@ class _InvoiceScreenState extends State<InvoiceScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final provider = Provider.of<InvoiceProvider>(context);
|
final provider = Provider.of<InvoiceProvider>(context);
|
||||||
|
|
||||||
|
final width = MediaQuery.of(context).size.width;
|
||||||
|
final scale = (width / 430).clamp(0.88, 1.08);
|
||||||
|
|
||||||
if (provider.loading) {
|
if (provider.loading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider.invoices.isEmpty) {
|
// 🔍 Filter invoices based on search query
|
||||||
return const Center(
|
final filteredInvoices = provider.invoices.where((inv) {
|
||||||
child: Text("No invoices found", style: TextStyle(fontSize: 18)));
|
final q = searchQuery.toLowerCase();
|
||||||
}
|
return inv['invoice_number'].toString().toLowerCase().contains(q) ||
|
||||||
|
inv['invoice_date'].toString().toLowerCase().contains(q) ||
|
||||||
|
inv['formatted_amount'].toString().toLowerCase().contains(q) ||
|
||||||
|
inv['status'].toString().toLowerCase().contains(q);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
return ListView.builder(
|
return Column(
|
||||||
padding: const EdgeInsets.all(16),
|
children: [
|
||||||
itemCount: provider.invoices.length,
|
// 🔍 SEARCH BAR
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.fromLTRB(16 * scale, 16 * scale, 16 * scale, 8 * scale),
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 14 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(14 * scale),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black12.withOpacity(0.08),
|
||||||
|
blurRadius: 8 * scale,
|
||||||
|
offset: Offset(0, 3 * scale),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
onChanged: (v) => setState(() => searchQuery = v),
|
||||||
|
style: TextStyle(fontSize: 14 * scale),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
icon: Icon(Icons.search, size: 22 * scale),
|
||||||
|
hintText: "Search Invoice Number, Date, Amount...",
|
||||||
|
border: InputBorder.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 📄 LIST OF INVOICES
|
||||||
|
Expanded(
|
||||||
|
child: filteredInvoices.isEmpty
|
||||||
|
? Center(
|
||||||
|
child: Text(
|
||||||
|
"No invoices found",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18 * scale,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ListView.builder(
|
||||||
|
padding: EdgeInsets.all(16 * scale),
|
||||||
|
itemCount: filteredInvoices.length,
|
||||||
itemBuilder: (_, i) {
|
itemBuilder: (_, i) {
|
||||||
final inv = provider.invoices[i];
|
final inv = filteredInvoices[i];
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
color: Colors.white,
|
||||||
child: Padding(
|
shape: RoundedRectangleBorder(
|
||||||
padding: const EdgeInsets.all(14),
|
borderRadius: BorderRadius.circular(16 * scale),
|
||||||
|
),
|
||||||
|
elevation: 3,
|
||||||
|
margin: EdgeInsets.only(bottom: 14 * scale),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.all(16 * scale),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
/// Invoice Number
|
||||||
Text(
|
Text(
|
||||||
"Invoice ${inv['invoice_number'] ?? 'N/A'}",
|
"Invoice ${inv['invoice_number'] ?? 'N/A'}",
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18, fontWeight: FontWeight.bold),
|
fontSize: 20 * scale,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 6),
|
SizedBox(height: 8 * scale),
|
||||||
Text("Date: ${inv['invoice_date'] ?? 'N/A'}"),
|
|
||||||
Text("Status: ${inv['status'] ?? 'N/A'}"),
|
|
||||||
Text("Amount: ₹${inv['formatted_amount'] ?? '0'}"),
|
|
||||||
|
|
||||||
const SizedBox(height: 10),
|
/// Date + Amount
|
||||||
|
Text(
|
||||||
|
"Date: ${inv['invoice_date'] ?? 'N/A'}",
|
||||||
|
style: TextStyle(fontSize: 15 * scale),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Amount: ₹${inv['formatted_amount'] ?? '0'}",
|
||||||
|
style: TextStyle(fontSize: 15 * scale),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 16 * scale),
|
||||||
|
|
||||||
|
/// BUTTONS ROW
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
OutlinedButton(
|
Expanded(
|
||||||
child: const Text("Invoice Details"),
|
child: GradientButton(
|
||||||
onPressed: () {
|
text: "Invoice Details",
|
||||||
|
fontSize: 15 * scale,
|
||||||
|
radius: 12 * scale,
|
||||||
|
padding: 14 * scale,
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Color(0xFF1976D2),
|
||||||
|
Color(0xFF42A5F5),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => InvoiceDetailScreen(
|
builder: (_) =>
|
||||||
invoiceId: inv['invoice_id'],
|
InvoiceDetailScreen(
|
||||||
),
|
invoiceId:
|
||||||
|
inv['invoice_id']),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
OutlinedButton(
|
SizedBox(width: 12 * scale),
|
||||||
child: const Text("Installments"),
|
|
||||||
onPressed: () {
|
Expanded(
|
||||||
|
child: GradientButton(
|
||||||
|
text: "Installments",
|
||||||
|
fontSize: 15 * scale,
|
||||||
|
radius: 12 * scale,
|
||||||
|
padding: 14 * scale,
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Color(0xFF43A047),
|
||||||
|
Color(0xFF81C784),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => InvoiceInstallmentScreen(
|
builder: (_) =>
|
||||||
invoiceId: inv['invoice_id'],
|
InvoiceInstallmentScreen(
|
||||||
),
|
invoiceId:
|
||||||
|
inv['invoice_id']),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
/// STATUS BADGE
|
||||||
|
Positioned(
|
||||||
|
right: 12 * scale,
|
||||||
|
top: 12 * scale,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 10 * scale,
|
||||||
|
vertical: 6 * scale,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _getStatusColor(inv['status']),
|
||||||
|
borderRadius: BorderRadius.circular(12 * scale),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
inv['status'] ?? 'N/A',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 12 * scale,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Status Color Helper
|
||||||
|
Color _getStatusColor(String? status) {
|
||||||
|
switch (status?.toLowerCase()) {
|
||||||
|
case 'pending':
|
||||||
|
return Colors.orange;
|
||||||
|
case 'in transit':
|
||||||
|
return Colors.blue;
|
||||||
|
case 'overdue':
|
||||||
|
return Colors.redAccent;
|
||||||
|
case 'paid':
|
||||||
|
return Colors.green;
|
||||||
|
default:
|
||||||
|
return Colors.grey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// -------------------------------------------------------
|
||||||
|
/// RESPONSIVE GRADIENT BUTTON
|
||||||
|
/// -------------------------------------------------------
|
||||||
|
class GradientButton extends StatelessWidget {
|
||||||
|
final String text;
|
||||||
|
final Gradient gradient;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
final double fontSize;
|
||||||
|
final double padding;
|
||||||
|
final double radius;
|
||||||
|
|
||||||
|
const GradientButton({
|
||||||
|
super.key,
|
||||||
|
required this.text,
|
||||||
|
required this.gradient,
|
||||||
|
required this.onTap,
|
||||||
|
required this.fontSize,
|
||||||
|
required this.padding,
|
||||||
|
required this.radius,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(radius),
|
||||||
|
onTap: onTap,
|
||||||
|
child: Ink(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: gradient,
|
||||||
|
borderRadius: BorderRadius.circular(radius),
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: padding),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: fontSize,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,27 +26,27 @@ 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);
|
||||||
|
|
||||||
final loginId = cLoginId.text.trim();
|
final id = cLoginId.text.trim();
|
||||||
final password = cPassword.text.trim();
|
final pass = cPassword.text.trim();
|
||||||
|
|
||||||
if (loginId.isEmpty || password.isEmpty) {
|
if (id.isEmpty || pass.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context)
|
||||||
const SnackBar(content: Text('Please enter login id and password')),
|
.showSnackBar(const SnackBar(content: Text("Please fill all fields")));
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final res = await auth.login(context, loginId, password);
|
final res = await auth.login(context, id, pass);
|
||||||
|
|
||||||
if (res['success'] == true) {
|
if (res['success'] == true) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.pushReplacement(
|
||||||
|
context,
|
||||||
MaterialPageRoute(builder: (_) => const MainBottomNav()),
|
MaterialPageRoute(builder: (_) => const MainBottomNav()),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final msg = res['message']?.toString() ?? 'Login failed';
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
if (!mounted) return;
|
SnackBar(content: Text(res['message'] ?? "Login failed")),
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,38 +55,113 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
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;
|
||||||
|
|
||||||
|
/// ⭐ RESPONSIVE SCALE
|
||||||
|
final scale = (width / 390).clamp(0.85, 1.25);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
backgroundColor: const Color(0xFFE8F0FF),
|
||||||
leading: IconButton(
|
|
||||||
icon: const Icon(Icons.arrow_back),
|
body: Stack(
|
||||||
onPressed: () {
|
children: [
|
||||||
|
/// 🔵 Floating Back Button (Responsive Position + Size)
|
||||||
|
Positioned(
|
||||||
|
top: 40 * scale,
|
||||||
|
left: 12 * scale,
|
||||||
|
child: Material(
|
||||||
|
elevation: 6 * scale,
|
||||||
|
color: Colors.indigo.shade700,
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (_) => const WelcomeScreen()),
|
MaterialPageRoute(builder: (_) => const WelcomeScreen()),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(10 * scale),
|
||||||
|
child: Icon(Icons.arrow_back,
|
||||||
|
color: Colors.white, size: 20 * scale),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
title: const Text('Login'),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
body: Padding(
|
/// 📦 Center White Card (Responsive)
|
||||||
padding: EdgeInsets.symmetric(horizontal: width * 0.06, vertical: 20),
|
Center(
|
||||||
child: Column(
|
child: Container(
|
||||||
children: [
|
width: width * 0.87,
|
||||||
RoundedInput(
|
padding: EdgeInsets.symmetric(
|
||||||
controller: cLoginId,
|
vertical: 28 * scale,
|
||||||
hint: 'Email / Mobile / Customer ID',
|
horizontal: 20 * scale,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
decoration: BoxDecoration(
|
||||||
RoundedInput(
|
color: Colors.white,
|
||||||
controller: cPassword,
|
borderRadius: BorderRadius.circular(22 * scale),
|
||||||
hint: 'Password',
|
boxShadow: [
|
||||||
obscure: true,
|
BoxShadow(
|
||||||
|
color: Colors.black12,
|
||||||
|
blurRadius: 18 * scale,
|
||||||
|
spreadRadius: 1,
|
||||||
|
offset: Offset(0, 6 * scale),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 18),
|
|
||||||
PrimaryButton(label: 'Login', onTap: _login, busy: auth.loading),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Login",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 26 * scale,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.indigo.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 25 * scale),
|
||||||
|
|
||||||
|
/// Login ID Input
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFD8E7FF),
|
||||||
|
borderRadius: BorderRadius.circular(14 * scale),
|
||||||
|
),
|
||||||
|
child: RoundedInput(
|
||||||
|
controller: cLoginId,
|
||||||
|
hint: "Email / Mobile / Customer ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 16 * scale),
|
||||||
|
|
||||||
|
/// Password Input
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFD8E7FF),
|
||||||
|
borderRadius: BorderRadius.circular(14 * scale),
|
||||||
|
),
|
||||||
|
child: RoundedInput(
|
||||||
|
controller: cPassword,
|
||||||
|
hint: "Password",
|
||||||
|
obscure: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 25 * scale),
|
||||||
|
|
||||||
|
/// Login Button
|
||||||
|
PrimaryButton(
|
||||||
|
label: "Login",
|
||||||
|
onTap: _login,
|
||||||
|
busy: auth.loading,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import 'order_screen.dart';
|
|||||||
import 'invoice_screen.dart';
|
import 'invoice_screen.dart';
|
||||||
import 'chat_screen.dart';
|
import 'chat_screen.dart';
|
||||||
import 'settings_screen.dart';
|
import 'settings_screen.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import '../providers/chat_unread_provider.dart';
|
||||||
|
|
||||||
|
|
||||||
class MainBottomNav extends StatefulWidget {
|
class MainBottomNav extends StatefulWidget {
|
||||||
const MainBottomNav({super.key});
|
const MainBottomNav({super.key});
|
||||||
@@ -17,12 +20,10 @@ class MainBottomNavState extends State<MainBottomNav> {
|
|||||||
int _currentIndex = 0;
|
int _currentIndex = 0;
|
||||||
|
|
||||||
void setIndex(int index) {
|
void setIndex(int index) {
|
||||||
setState(() {
|
setState(() => _currentIndex = index);
|
||||||
_currentIndex = index;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<Widget> _screens = const [
|
final List<Widget> _screens = [
|
||||||
DashboardScreen(),
|
DashboardScreen(),
|
||||||
OrdersScreen(),
|
OrdersScreen(),
|
||||||
InvoiceScreen(),
|
InvoiceScreen(),
|
||||||
@@ -30,27 +31,210 @@ class MainBottomNavState extends State<MainBottomNav> {
|
|||||||
SettingsScreen(),
|
SettingsScreen(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
final List<IconData> _icons = const [
|
||||||
|
Icons.dashboard_outlined,
|
||||||
|
Icons.shopping_bag_outlined,
|
||||||
|
Icons.receipt_long_outlined,
|
||||||
|
Icons.chat_bubble_outline,
|
||||||
|
Icons.settings_outlined,
|
||||||
|
];
|
||||||
|
|
||||||
|
final List<String> _labels = const [
|
||||||
|
"Dashboard",
|
||||||
|
"Orders",
|
||||||
|
"Invoice",
|
||||||
|
"Chat",
|
||||||
|
"Settings",
|
||||||
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final width = MediaQuery.of(context).size.width;
|
||||||
|
final scale = (width / 390).clamp(0.85, 1.20);
|
||||||
|
|
||||||
|
final containerPadding = 8 * scale;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: const MainAppBar(),
|
appBar: const MainAppBar(),
|
||||||
body: _screens[_currentIndex],
|
body: _screens[_currentIndex],
|
||||||
bottomNavigationBar: BottomNavigationBar(
|
|
||||||
currentIndex: _currentIndex,
|
bottomNavigationBar: Padding(
|
||||||
selectedItemColor: Colors.red,
|
padding: EdgeInsets.only(
|
||||||
unselectedItemColor: Colors.black,
|
left: 10 * scale,
|
||||||
type: BottomNavigationBarType.fixed,
|
right: 10 * scale,
|
||||||
onTap: (index) {
|
bottom: 10 * scale,
|
||||||
setState(() => _currentIndex = index);
|
),
|
||||||
},
|
child: LayoutBuilder(
|
||||||
items: const [
|
builder: (context, constraints) {
|
||||||
BottomNavigationBarItem(icon: Icon(Icons.dashboard_outlined), label: "Dashboard"),
|
final totalWidth = constraints.maxWidth;
|
||||||
BottomNavigationBarItem(icon: Icon(Icons.shopping_bag_outlined), label: "Orders"),
|
|
||||||
BottomNavigationBarItem(icon: Icon(Icons.receipt_long_outlined), label: "Invoice"),
|
// inner width (after padding)
|
||||||
BottomNavigationBarItem(icon: Icon(Icons.chat_bubble_outline), label: "Chat"),
|
final contentWidth = totalWidth - (containerPadding * 2);
|
||||||
BottomNavigationBarItem(icon: Icon(Icons.settings_outlined), label: "Settings"),
|
|
||||||
|
final safeContentWidth =
|
||||||
|
contentWidth > 0 ? contentWidth : totalWidth;
|
||||||
|
|
||||||
|
final itemWidth = safeContentWidth / _icons.length;
|
||||||
|
|
||||||
|
final indicatorWidth = 70 * scale;
|
||||||
|
final indicatorHeight = 70 * scale;
|
||||||
|
|
||||||
|
double left = (_currentIndex * itemWidth) +
|
||||||
|
(itemWidth / 2) -
|
||||||
|
(indicatorWidth / 2);
|
||||||
|
|
||||||
|
/// ⭐ FIX: explicitly convert clamp to double
|
||||||
|
final double safeLeft = left
|
||||||
|
.clamp(0, safeContentWidth - indicatorWidth)
|
||||||
|
.toDouble();
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
height: 100 * scale,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: containerPadding),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(28 * scale),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.08),
|
||||||
|
blurRadius: 20 * scale,
|
||||||
|
offset: Offset(0, 8 * scale),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
/// ⭐ Indicator - safe positioned
|
||||||
|
AnimatedPositioned(
|
||||||
|
duration: const Duration(milliseconds: 350),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
top: 10 * scale,
|
||||||
|
left: safeLeft,
|
||||||
|
child: Container(
|
||||||
|
width: indicatorWidth,
|
||||||
|
height: indicatorHeight,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Color(0xFF4F46E5),
|
||||||
|
Color(0xFF06B6D4),
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(20 * scale),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
_icons[_currentIndex],
|
||||||
|
size: 22 * scale,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
SizedBox(height: 2 * scale),
|
||||||
|
Text(
|
||||||
|
_labels[_currentIndex],
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10 * scale,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
/// ⭐ Icon Row
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: List.generate(_icons.length, (index) {
|
||||||
|
final selected = index == _currentIndex;
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => setIndex(index),
|
||||||
|
child: SizedBox(
|
||||||
|
width: itemWidth,
|
||||||
|
height: 100 * scale,
|
||||||
|
child: Center(
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
opacity: selected ? 0 : 1,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
index == 3
|
||||||
|
? Consumer<ChatUnreadProvider>(
|
||||||
|
builder: (_, chat, __) {
|
||||||
|
return Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
_icons[index],
|
||||||
|
size: 22 * scale,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
|
||||||
|
if (chat.unreadCount > 0)
|
||||||
|
Positioned(
|
||||||
|
right: -6,
|
||||||
|
top: -6,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.red,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 20,
|
||||||
|
minHeight: 20,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
chat.unreadCount.toString(),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: Icon(
|
||||||
|
_icons[index],
|
||||||
|
size: 22 * scale,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 4 * scale),
|
||||||
|
Text(
|
||||||
|
_labels[index],
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10 * scale,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,15 +10,13 @@ class MarkListScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MarkListScreenState extends State<MarkListScreen> {
|
class _MarkListScreenState extends State<MarkListScreen> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
final provider = Provider.of<MarkListProvider>(context, listen: false);
|
final provider = Provider.of<MarkListProvider>(context, listen: false);
|
||||||
provider.init(context);
|
provider.init(context);
|
||||||
provider.loadMarks(context); // Load full list again
|
provider.loadMarks(context);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,27 +24,121 @@ class _MarkListScreenState extends State<MarkListScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final marks = Provider.of<MarkListProvider>(context);
|
final marks = Provider.of<MarkListProvider>(context);
|
||||||
|
|
||||||
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
|
final scale = (screenWidth / 390).clamp(0.82, 1.35);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text("All Mark Numbers"),
|
backgroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
centerTitle: true,
|
||||||
|
iconTheme: const IconThemeData(color: Colors.black),
|
||||||
|
title: Text(
|
||||||
|
"All Mark Numbers",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 20 * scale,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
body: marks.loading
|
body: marks.loading
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: ListView.builder(
|
: ListView.builder(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: EdgeInsets.all(10 * scale),
|
||||||
itemCount: marks.marks.length,
|
itemCount: marks.marks.length,
|
||||||
itemBuilder: (_, i) {
|
itemBuilder: (_, i) {
|
||||||
final m = marks.marks[i];
|
final m = marks.marks[i];
|
||||||
|
final status =
|
||||||
|
(m['status'] ?? '').toString().toLowerCase();
|
||||||
|
|
||||||
return Card(
|
final LinearGradient statusGradient =
|
||||||
child: ListTile(
|
status == 'active'
|
||||||
title: Text(m['mark_no']),
|
? const LinearGradient(
|
||||||
subtitle: Text("${m['origin']} → ${m['destination']}"),
|
colors: [
|
||||||
trailing: Text(
|
Color(0xFF2ECC71), // Green
|
||||||
m['status'],
|
Color(0xFF1E8449), // Deep Emerald
|
||||||
style: const TextStyle(color: Colors.indigo),
|
],
|
||||||
|
)
|
||||||
|
: const LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Color(0xFFE74C3C), // Red
|
||||||
|
Color(0xFFC0392B), // Dark Red
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: EdgeInsets.only(bottom: 10 * scale),
|
||||||
|
padding: EdgeInsets.all(12 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Color(0xFF2196F3),
|
||||||
|
Color(0xFF64B5F6),
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
),
|
),
|
||||||
|
borderRadius: BorderRadius.circular(14 * scale),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.06),
|
||||||
|
blurRadius: 5 * scale,
|
||||||
|
offset: Offset(0, 2 * scale),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// LEFT TEXT
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
m['mark_no'],
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 16 * scale,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 3 * scale),
|
||||||
|
Text(
|
||||||
|
"${m['origin']} → ${m['destination']}",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 13 * scale,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// STATUS BADGE (GREEN / RED)
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 10 * scale,
|
||||||
|
vertical: 5 * scale,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: statusGradient,
|
||||||
|
borderRadius: BorderRadius.circular(20 * scale),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
m['status'],
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11.5 * scale,
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import 'dart:ui';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../services/dio_client.dart';
|
import '../services/dio_client.dart';
|
||||||
import '../services/order_service.dart';
|
import '../services/order_service.dart';
|
||||||
|
|
||||||
|
|
||||||
class OrderDetailScreen extends StatefulWidget {
|
class OrderDetailScreen extends StatefulWidget {
|
||||||
final String orderId;
|
final String orderId;
|
||||||
const OrderDetailScreen({super.key, required this.orderId});
|
const OrderDetailScreen({super.key, required this.orderId});
|
||||||
@@ -14,6 +14,13 @@ class OrderDetailScreen extends StatefulWidget {
|
|||||||
class _OrderDetailScreenState extends State<OrderDetailScreen> {
|
class _OrderDetailScreenState extends State<OrderDetailScreen> {
|
||||||
bool loading = true;
|
bool loading = true;
|
||||||
Map order = {};
|
Map order = {};
|
||||||
|
final Map<String, bool> _expanded = {};
|
||||||
|
|
||||||
|
bool confirming = false;
|
||||||
|
|
||||||
|
bool get isOrderPlaced => order['status'] == 'order_placed';
|
||||||
|
bool get isConfirmed => order['status'] != 'order_placed';
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -33,105 +40,365 @@ class _OrderDetailScreenState extends State<OrderDetailScreen> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _row(String label, dynamic value) {
|
String _initials(String? s) {
|
||||||
return Padding(
|
if (s == null || s.isEmpty) return "I";
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
final parts = s.split(" ");
|
||||||
child: Row(
|
return parts.take(2).map((e) => e[0].toUpperCase()).join();
|
||||||
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final items = order['items'] ?? [];
|
final items = order['items'] ?? [];
|
||||||
|
|
||||||
|
final width = MediaQuery.of(context).size.width;
|
||||||
|
final scale = (width / 430).clamp(0.85, 1.20);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text("Order Details")),
|
backgroundColor: const Color(0xFFF0F6FF),
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Order Details"),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
body: loading
|
body: loading
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: Padding(
|
: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: EdgeInsets.all(16 * scale),
|
||||||
child: ListView(
|
child: SingleChildScrollView(
|
||||||
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(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
Text(
|
_summaryCard(scale),
|
||||||
item['description'] ?? "No description",
|
SizedBox(height: 18 * scale),
|
||||||
style: const TextStyle(
|
_itemsSection(items, scale),
|
||||||
fontSize: 16,
|
SizedBox(height: 18 * scale),
|
||||||
fontWeight: FontWeight.w600),
|
_totalsSection(scale),
|
||||||
),
|
|
||||||
const SizedBox(height: 6),
|
SizedBox(height: 24 * scale),
|
||||||
|
_confirmOrderButton(scale),
|
||||||
|
|
||||||
_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']),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _confirmOrderButton(double scale) {
|
||||||
|
final isPlaced = order['status'] == 'order_placed';
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 52 * scale,
|
||||||
|
child: ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: isPlaced ? Colors.orange : Colors.green,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14 * scale),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: (!isPlaced || confirming)
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
setState(() => confirming = true);
|
||||||
|
|
||||||
|
final service =
|
||||||
|
OrderService(DioClient.getInstance(context));
|
||||||
|
|
||||||
|
final res =
|
||||||
|
await service.confirmOrder(order['order_id']);
|
||||||
|
|
||||||
|
confirming = false;
|
||||||
|
|
||||||
|
if (res['success'] == true) {
|
||||||
|
setState(() {
|
||||||
|
order['status'] = 'order_confirmed';
|
||||||
|
});
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Order confirmed successfully'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(res['message'] ?? 'Failed to confirm order'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
child: confirming
|
||||||
|
? const CircularProgressIndicator(color: Colors.white)
|
||||||
|
: Text(
|
||||||
|
isPlaced ? 'Confirm Order' : 'Order Confirmed',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16 * scale,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// SUMMARY CARD
|
||||||
|
// -----------------------------
|
||||||
|
Widget _summaryCard(double scale) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(18 * scale),
|
||||||
|
decoration: _cardDecoration(scale),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text("Order Summary",
|
||||||
|
style: TextStyle(fontSize: 20 * scale, fontWeight: FontWeight.bold)),
|
||||||
|
SizedBox(height: 12 * scale),
|
||||||
|
_infoRow("Order ID", order['order_id'], scale),
|
||||||
|
_infoRow("Mark No", order['mark_no'], scale),
|
||||||
|
_infoRow("Origin", order['origin'], scale),
|
||||||
|
_infoRow("Destination", order['destination'], scale),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _infoRow(String title, dynamic value, double scale) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 6 * scale),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(title,
|
||||||
|
style: TextStyle(color: Colors.grey, fontSize: 14 * scale)),
|
||||||
|
Text(value?.toString() ?? "-",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15 * scale)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// ORDER ITEMS SECTION
|
||||||
|
// -----------------------------
|
||||||
|
Widget _itemsSection(List items, double scale) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(18 * scale),
|
||||||
|
decoration: _cardDecoration(scale),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text("Order Items",
|
||||||
|
style: TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
|
||||||
|
SizedBox(height: 16 * scale),
|
||||||
|
...List.generate(items.length, (i) {
|
||||||
|
return _expandableItem(items[i], i, scale);
|
||||||
|
})
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// EXPANDABLE ITEM
|
||||||
|
// -----------------------------
|
||||||
|
Widget _expandableItem(Map item, int index, double scale) {
|
||||||
|
final id = "item_$index";
|
||||||
|
_expanded[id] = _expanded[id] ?? false;
|
||||||
|
|
||||||
|
final description = item['description'] ?? "Item";
|
||||||
|
final initials = _initials(description);
|
||||||
|
final imageUrl = item['image'] ?? "";
|
||||||
|
|
||||||
|
return AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
margin: EdgeInsets.only(bottom: 16 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(14 * scale),
|
||||||
|
border: Border.all(color: Colors.black12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
minVerticalPadding: 10 * scale,
|
||||||
|
leading: _avatar(imageUrl, initials, scale),
|
||||||
|
title: Text(description,
|
||||||
|
style:
|
||||||
|
TextStyle(fontWeight: FontWeight.bold, fontSize: 15 * scale)),
|
||||||
|
trailing: Transform.rotate(
|
||||||
|
angle: (_expanded[id]! ? 3.14 : 0),
|
||||||
|
child: Icon(Icons.keyboard_arrow_down, size: 24 * scale),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_expanded[id] = !_expanded[id]!;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
if (_expanded[id]!)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.all(12 * scale),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_pill("Qty", Icons.list_alt, "${item['qty']}",
|
||||||
|
Colors.blue.shade100, scale),
|
||||||
|
_pill("Unit", Icons.category, "${item['unit']}",
|
||||||
|
Colors.orange.shade100, scale),
|
||||||
|
_pill("KG", Icons.scale, "${item['kg']}",
|
||||||
|
Colors.red.shade100, scale),
|
||||||
|
_pill("CBM", Icons.straighten, "${item['cbm']}",
|
||||||
|
Colors.purple.shade100, scale),
|
||||||
|
_pill("Shop", Icons.storefront, "${item['shop_no']}",
|
||||||
|
Colors.grey.shade300, scale),
|
||||||
|
_pill("Amount", Icons.currency_rupee,
|
||||||
|
"₹${item['ttl_amount']}",
|
||||||
|
Colors.green.shade100, scale),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// AVATAR (RESPONSIVE)
|
||||||
|
// -----------------------------
|
||||||
|
Widget _avatar(String url, String initials, double scale) {
|
||||||
|
return Container(
|
||||||
|
width: 48 * scale,
|
||||||
|
height: 48 * scale,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue.shade200,
|
||||||
|
borderRadius: BorderRadius.circular(10 * scale),
|
||||||
|
),
|
||||||
|
child: url.isNotEmpty
|
||||||
|
? ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(10 * scale),
|
||||||
|
child: Image.network(url, fit: BoxFit.cover,
|
||||||
|
errorBuilder: (c, e, s) => Center(
|
||||||
|
child: Text(initials,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18 * scale,
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
: Center(
|
||||||
|
child: Text(initials,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18 * scale,
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// COLOR-CODED PILL (RESPONSIVE)
|
||||||
|
// -----------------------------
|
||||||
|
Widget _pill(
|
||||||
|
String title, IconData icon, String value, Color bgColor, double scale) {
|
||||||
|
return Container(
|
||||||
|
margin: EdgeInsets.only(bottom: 12 * scale),
|
||||||
|
padding:
|
||||||
|
EdgeInsets.symmetric(horizontal: 14 * scale, vertical: 12 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: bgColor,
|
||||||
|
borderRadius: BorderRadius.circular(12 * scale),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 20 * scale, color: Colors.black54),
|
||||||
|
SizedBox(width: 10 * scale),
|
||||||
|
Text("$title: ",
|
||||||
|
style:
|
||||||
|
TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * scale)),
|
||||||
|
Expanded(
|
||||||
|
child: Text(value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w700, fontSize: 15 * scale)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// TOTAL SECTION
|
||||||
|
// -----------------------------
|
||||||
|
Widget _totalsSection(double scale) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(18 * scale),
|
||||||
|
decoration: _cardDecoration(scale),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text("Totalss",
|
||||||
|
style: TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
|
||||||
|
SizedBox(height: 12 * scale),
|
||||||
|
_totalRow("Total Qty", order['ttl_qty'], scale),
|
||||||
|
_totalRow("Total KG", order['ttl_kg'], scale),
|
||||||
|
SizedBox(height: 12 * scale),
|
||||||
|
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 20 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.green.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(12 * scale),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text("₹${order['ttl_amount'] ?? 0}",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.green,
|
||||||
|
fontSize: 28 * scale,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
)),
|
||||||
|
SizedBox(height: 4 * scale),
|
||||||
|
Text("Total Amount",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.black54, fontSize: 14 * scale)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _totalRow(String title, dynamic value, double scale) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(title,
|
||||||
|
style: TextStyle(color: Colors.grey, fontSize: 14 * scale)),
|
||||||
|
Text(value?.toString() ?? "0",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15 * scale)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// CARD DECORATION
|
||||||
|
// -----------------------------
|
||||||
|
BoxDecoration _cardDecoration(double scale) {
|
||||||
|
return BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16 * scale),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black12,
|
||||||
|
blurRadius: 8 * scale,
|
||||||
|
offset: Offset(0, 3 * scale)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
|||||||
import '../services/dio_client.dart';
|
import '../services/dio_client.dart';
|
||||||
import '../services/order_service.dart';
|
import '../services/order_service.dart';
|
||||||
|
|
||||||
|
|
||||||
class OrderInvoiceScreen extends StatefulWidget {
|
class OrderInvoiceScreen extends StatefulWidget {
|
||||||
final String orderId;
|
final String orderId;
|
||||||
const OrderInvoiceScreen({super.key, required this.orderId});
|
const OrderInvoiceScreen({super.key, required this.orderId});
|
||||||
@@ -11,139 +10,442 @@ class OrderInvoiceScreen extends StatefulWidget {
|
|||||||
State<OrderInvoiceScreen> createState() => _OrderInvoiceScreenState();
|
State<OrderInvoiceScreen> createState() => _OrderInvoiceScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _OrderInvoiceScreenState extends State<OrderInvoiceScreen> {
|
class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
bool loading = true;
|
bool loading = true;
|
||||||
|
bool controllerInitialized = false;
|
||||||
|
|
||||||
Map invoice = {};
|
Map invoice = {};
|
||||||
|
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<Offset> _slideAnimation;
|
||||||
|
|
||||||
|
bool s1 = true;
|
||||||
|
bool s2 = false;
|
||||||
|
bool s3 = false;
|
||||||
|
bool s4 = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
initializeController();
|
||||||
load();
|
load();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void initializeController() {
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 280),
|
||||||
|
);
|
||||||
|
|
||||||
|
_slideAnimation = Tween<Offset>(
|
||||||
|
begin: const Offset(0, -0.05),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(
|
||||||
|
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
|
||||||
|
);
|
||||||
|
|
||||||
|
controllerInitialized = true;
|
||||||
|
_controller.forward();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (controllerInitialized) _controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> load() async {
|
Future<void> load() async {
|
||||||
final service = OrderService(DioClient.getInstance(context));
|
final service = OrderService(DioClient.getInstance(context));
|
||||||
final res = await service.getInvoice(widget.orderId);
|
final res = await service.getInvoice(widget.orderId);
|
||||||
|
|
||||||
if (res['success'] == true) {
|
if (res["success"] == true) {
|
||||||
invoice = res['invoice'] ?? {};
|
invoice = res["invoice"] ?? {};
|
||||||
}
|
}
|
||||||
|
|
||||||
loading = false;
|
if (mounted) setState(() => 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final items = invoice['items'] ?? [];
|
final items = invoice["items"] as List? ?? [];
|
||||||
|
|
||||||
|
final width = MediaQuery.of(context).size.width;
|
||||||
|
final scale = (width / 430).clamp(0.85, 1.18);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text("Invoice")),
|
appBar: AppBar(
|
||||||
body: loading
|
title: const Text("Invoice"),
|
||||||
? 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"),
|
|
||||||
),
|
),
|
||||||
|
body: loading || !controllerInitialized
|
||||||
const SizedBox(height: 20),
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: ListView(
|
||||||
const Text("Invoice Items",
|
padding: EdgeInsets.all(16 * scale),
|
||||||
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: [
|
children: [
|
||||||
Text("Qty: ${item['qty'] ?? 0}"),
|
_headerCard(scale),
|
||||||
Text("Price: ₹${item['price'] ?? 0}"),
|
|
||||||
|
_sectionHeader("Invoice Summary", Icons.receipt, s1, () {
|
||||||
|
setState(() => s1 = !s1);
|
||||||
|
}, scale),
|
||||||
|
_sectionBody(s1, [
|
||||||
|
_detailRow(Icons.numbers, "Invoice No", invoice['invoice_number'], scale),
|
||||||
|
_detailRow(Icons.calendar_month, "Invoice Date", invoice['invoice_date'], scale),
|
||||||
|
_detailRow(Icons.date_range, "Due Date", invoice['due_date'], scale),
|
||||||
|
], scale),
|
||||||
|
|
||||||
|
_sectionHeader("Amount Details", Icons.currency_rupee, s2, () {
|
||||||
|
setState(() => s2 = !s2);
|
||||||
|
}, scale),
|
||||||
|
_sectionBody(s2, [
|
||||||
|
_detailRow(Icons.money, "Amount", invoice['final_amount'], scale),
|
||||||
|
_detailRow(Icons.percent, "GST percent", invoice['gst_percent'], scale),
|
||||||
|
_detailRow(Icons.percent, "GST Amount", invoice['gst_amount'], scale),
|
||||||
|
_detailRow(Icons.summarize, "Final With GST",
|
||||||
|
invoice['final_amount_with_gst'], scale),
|
||||||
|
], scale),
|
||||||
|
|
||||||
|
_sectionHeader("Customer Details", Icons.person, s3, () {
|
||||||
|
setState(() => s3 = !s3);
|
||||||
|
}, scale),
|
||||||
|
_sectionBody(s3, [
|
||||||
|
_detailRow(Icons.person, "Name", invoice['customer_name'], scale),
|
||||||
|
_detailRow(Icons.business, "Company", invoice['company_name'], scale),
|
||||||
|
_detailRow(Icons.mail, "Email", invoice['customer_email'], scale),
|
||||||
|
_detailRow(Icons.phone, "Mobile", invoice['customer_mobile'], scale),
|
||||||
|
_detailRow(Icons.location_on, "Address", invoice['customer_address'], scale),
|
||||||
|
], scale),
|
||||||
|
|
||||||
|
_sectionHeader("Invoice Items", Icons.shopping_cart, s4, () {
|
||||||
|
setState(() => s4 = !s4);
|
||||||
|
}, scale),
|
||||||
|
_sectionBody(
|
||||||
|
s4,
|
||||||
|
items.isEmpty
|
||||||
|
? [Text("No items found", style: TextStyle(fontSize: 14 * scale))]
|
||||||
|
: items.map((item) => _itemTile(item, scale)).toList(),
|
||||||
|
scale,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
trailing: Text(
|
);
|
||||||
"₹${item['ttl_amount'] ?? 0}",
|
}
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
// ---------------- HEADER CARD ----------------
|
||||||
color: Colors.indigo),
|
Widget _headerCard(double scale) {
|
||||||
|
final statusColor = getInvoiceStatusColor(invoice["status"]);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(18 * scale),
|
||||||
|
margin: EdgeInsets.only(bottom: 18 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [Colors.indigo.shade400, Colors.blue.shade600],
|
||||||
),
|
),
|
||||||
|
borderRadius: BorderRadius.circular(16 * scale),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
blurRadius: 10 * scale,
|
||||||
|
color: Colors.black.withOpacity(.15),
|
||||||
|
offset: Offset(0, 3 * scale),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Invoice #${invoice['invoice_number'] ?? '-'}",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 22 * scale,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 6 * scale),
|
||||||
|
Text(
|
||||||
|
"Date: ${invoice['invoice_date'] ?? '-'}",
|
||||||
|
style: TextStyle(color: Colors.white70, fontSize: 14 * scale),
|
||||||
|
),
|
||||||
|
SizedBox(height: 10 * scale),
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
vertical: 6 * scale, horizontal: 14 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white, // ✅ Always white
|
||||||
|
borderRadius: BorderRadius.circular(50 * scale),
|
||||||
|
border: Border.all(
|
||||||
|
color: statusColor, // ✅ Different for each status
|
||||||
|
width: 1.4 * scale,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
(invoice["status"] ?? "Unknown").toString().toUpperCase(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: statusColor, // ✅ Text color changes
|
||||||
|
fontSize: 14 * scale,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}
|
||||||
|
|
||||||
|
// ---------------- SECTION HEADER ----------------
|
||||||
|
Widget _sectionHeader(
|
||||||
|
String title, IconData icon, bool expanded, Function toggle, double scale) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => toggle(),
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.all(14 * scale),
|
||||||
|
margin: EdgeInsets.only(bottom: 10 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient:
|
||||||
|
LinearGradient(colors: [Colors.blue.shade400, Colors.indigo.shade500]),
|
||||||
|
borderRadius: BorderRadius.circular(14 * scale),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: Colors.white, size: 20 * scale),
|
||||||
|
SizedBox(width: 10 * scale),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 15 * scale,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
AnimatedRotation(
|
||||||
|
turns: expanded ? .5 : 0,
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
child: Icon(Icons.keyboard_arrow_down,
|
||||||
|
color: Colors.white, size: 22 * scale),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------- SECTION BODY ----------------
|
||||||
|
Widget _sectionBody(bool visible, List<Widget> children, double scale) {
|
||||||
|
if (!controllerInitialized) return const SizedBox();
|
||||||
|
|
||||||
|
return AnimatedCrossFade(
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
firstChild: const SizedBox.shrink(),
|
||||||
|
secondChild: SlideTransition(
|
||||||
|
position: _slideAnimation,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.all(16 * scale),
|
||||||
|
margin: EdgeInsets.only(bottom: 14 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(14 * scale),
|
||||||
|
color: Colors.white,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
blurRadius: 8 * scale,
|
||||||
|
offset: Offset(0, 3 * scale),
|
||||||
|
color: Colors.black.withOpacity(.08),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(children: children),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
crossFadeState:
|
||||||
|
visible ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- DETAIL ROW ----------------
|
||||||
|
Widget _detailRow(IconData icon, String label, dynamic value, double scale) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 6 * scale),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: Colors.blueGrey, size: 20 * scale),
|
||||||
|
SizedBox(width: 10 * scale),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(color: Colors.grey.shade700, fontSize: 14 * scale),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
value?.toString() ?? "N/A",
|
||||||
|
textAlign: TextAlign.end,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 15 * scale,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- ITEM TILE ----------------
|
||||||
|
Widget _itemTile(Map item, double scale) {
|
||||||
|
final qty = item['qty'] ?? 0;
|
||||||
|
final price = item['price'] ?? 0;
|
||||||
|
final total = item['ttl_amount'] ?? 0;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(16 * scale),
|
||||||
|
margin: EdgeInsets.only(bottom: 14 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(16 * scale),
|
||||||
|
color: Colors.white,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
blurRadius: 8 * scale,
|
||||||
|
offset: Offset(0, 3 * scale),
|
||||||
|
color: Colors.black.withOpacity(.08),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
border: Border.all(color: Colors.grey.shade300, width: 1),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(8 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.indigo.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(10 * scale),
|
||||||
|
),
|
||||||
|
child: Icon(Icons.inventory_2,
|
||||||
|
color: Colors.indigo, size: 20 * scale),
|
||||||
|
),
|
||||||
|
SizedBox(width: 12 * scale),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
item['description'] ?? "Item",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16 * scale,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 14 * scale),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
_itemBadge(Icons.numbers, "Qty", qty.toString(), false, scale),
|
||||||
|
_itemBadge(Icons.currency_rupee, "Price", "₹$price", false, scale),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 12 * scale),
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
vertical: 10 * scale, horizontal: 14 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(12 * scale),
|
||||||
|
color: Colors.indigo.shade50,
|
||||||
|
border: Border.all(color: Colors.indigo, width: 1.5 * scale),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.summarize,
|
||||||
|
size: 18 * scale, color: Colors.indigo),
|
||||||
|
SizedBox(width: 6 * scale),
|
||||||
|
Text(
|
||||||
|
"Total:",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15 * scale,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.indigo,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"₹$total",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 17 * scale,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.indigo,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- BADGE ----------------
|
||||||
|
Widget _itemBadge(
|
||||||
|
IconData icon, String label, String value, bool highlight, double scale) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 8 * scale, horizontal: 12 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(12 * scale),
|
||||||
|
color: highlight ? Colors.indigo.shade50 : Colors.grey.shade100,
|
||||||
|
border: Border.all(
|
||||||
|
color: highlight ? Colors.indigo : Colors.grey.shade300,
|
||||||
|
width: highlight ? 1.5 * scale : 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon,
|
||||||
|
size: 16 * scale,
|
||||||
|
color: highlight ? Colors.indigo : Colors.grey),
|
||||||
|
SizedBox(width: 4 * scale),
|
||||||
|
Text(
|
||||||
|
"$label: ",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13 * scale,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: highlight ? Colors.indigo : Colors.grey.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14 * scale,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: highlight ? Colors.indigo : Colors.black,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- STATUS COLOR HELPER ----------------
|
||||||
|
|
||||||
|
Color getInvoiceStatusColor(String? status) {
|
||||||
|
final s = (status ?? '')
|
||||||
|
.toLowerCase()
|
||||||
|
.replaceAll('_', ' ')
|
||||||
|
.replaceAll('-', ' ')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
if (s == 'paid') return Colors.green.shade600;
|
||||||
|
if (s == 'pending') return Colors.orange.shade600;
|
||||||
|
if (s == 'overdue') return Colors.red.shade600;
|
||||||
|
if (s == 'cancelled' || s == 'canceled') return Colors.grey.shade600;
|
||||||
|
if (s == 'in progress') return Colors.blue.shade600;
|
||||||
|
if (s == 'draft') return Colors.purple.shade600;
|
||||||
|
|
||||||
|
return Colors.blueGrey;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../providers/order_provider.dart';
|
import '../providers/order_provider.dart';
|
||||||
import 'order_detail_screen.dart';
|
import 'order_detail_screen.dart';
|
||||||
import 'order_shipment_screen.dart';
|
|
||||||
import 'order_invoice_screen.dart';
|
import 'order_invoice_screen.dart';
|
||||||
import 'order_track_screen.dart';
|
import 'order_track_screen.dart';
|
||||||
|
|
||||||
@@ -14,6 +13,7 @@ class OrdersScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _OrdersScreenState extends State<OrdersScreen> {
|
class _OrdersScreenState extends State<OrdersScreen> {
|
||||||
|
String searchQuery = "";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -31,77 +31,359 @@ class _OrdersScreenState extends State<OrdersScreen> {
|
|||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
return ListView.builder(
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
padding: const EdgeInsets.all(16),
|
final scale = (screenWidth / 420).clamp(0.85, 1.1);
|
||||||
itemCount: provider.orders.length,
|
|
||||||
itemBuilder: (_, i) {
|
final filteredOrders = provider.orders.where((o) {
|
||||||
final o = provider.orders[i];
|
final q = searchQuery.toLowerCase();
|
||||||
|
return o["order_id"].toString().toLowerCase().contains(q) ||
|
||||||
|
o["status"].toString().toLowerCase().contains(q) ||
|
||||||
|
o["description"].toString().toLowerCase().contains(q);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_searchBar(scale),
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: EdgeInsets.all(14 * scale),
|
||||||
|
itemCount: filteredOrders.length,
|
||||||
|
itemBuilder: (context, i) {
|
||||||
|
return _orderCard(filteredOrders[i], scale);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _searchBar(double scale) {
|
||||||
|
return Container(
|
||||||
|
margin: EdgeInsets.fromLTRB(14 * scale, 14 * scale, 14 * scale, 8 * scale),
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 12 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(14 * scale),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black12.withOpacity(0.1),
|
||||||
|
blurRadius: 8 * scale,
|
||||||
|
offset: const Offset(0, 3),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.search, size: 20 * scale, color: Colors.grey.shade700),
|
||||||
|
SizedBox(width: 8 * scale),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
onChanged: (v) => setState(() => searchQuery = v),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: "Search orders...",
|
||||||
|
border: InputBorder.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _orderCard(Map<String, dynamic> o, double scale) {
|
||||||
|
final progress = getProgress(o['status']);
|
||||||
|
final percent = (progress * 100).toInt();
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
margin: EdgeInsets.only(bottom: 12 * scale),
|
||||||
|
color: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: EdgeInsets.all(12 * scale), // 👈 tighter padding
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text("Order ID: ${o['order_id']}",
|
// HEADER
|
||||||
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(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
_btn("Order", () => _openOrderDetails(o['order_id'])),
|
Text(
|
||||||
_btn("Shipment", () => _openShipment(o['order_id'])),
|
"Order #${o['order_id']}",
|
||||||
_btn("Invoice", () => _openInvoice(o['order_id'])),
|
style: TextStyle(
|
||||||
_btn("Track", () => _openTrack(o['order_id'])),
|
fontSize: 15 * scale, // 👈 slightly smaller
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
getStatusBadge(o['status'], scale),
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 6 * scale),
|
||||||
|
Text(
|
||||||
|
o['description'],
|
||||||
|
style: TextStyle(fontSize: 13 * scale),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 4 * scale),
|
||||||
|
Text(
|
||||||
|
"₹${o['amount']}",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15 * scale,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 12 * scale),
|
||||||
|
|
||||||
|
// PROGRESS HEADER
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Text(
|
||||||
|
"$percent%",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 11 * scale,
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
_AnimatedProgressBar(progress: progress, scale: scale),
|
||||||
|
|
||||||
|
SizedBox(height: 6 * scale),
|
||||||
|
|
||||||
|
// PROGRESS LABELS
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: const [
|
||||||
|
Text("Shipment Ready",
|
||||||
|
style: TextStyle(fontSize: 10, color: Colors.grey)),
|
||||||
|
Text("Import Custom",
|
||||||
|
style: TextStyle(fontSize: 10, color: Colors.grey)),
|
||||||
|
Text("Delivered",
|
||||||
|
style: TextStyle(fontSize: 10, color: Colors.grey)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 12 * scale),
|
||||||
|
|
||||||
|
// ACTIONS
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
_btn(Icons.visibility, "View",
|
||||||
|
Colors.green.shade700, Colors.green.shade50,
|
||||||
|
() => _openOrderDetails(o['order_id']), scale),
|
||||||
|
_btn(Icons.receipt_long, "Invoice",
|
||||||
|
Colors.orange.shade700, Colors.orange.shade50,
|
||||||
|
() => _openInvoice(o['order_id']), scale),
|
||||||
|
_btn(Icons.local_shipping, "Track",
|
||||||
|
Colors.blue.shade700, Colors.blue.shade50,
|
||||||
|
() => _openTrack(o['order_id']), scale),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _btn(String text, VoidCallback onTap) {
|
Widget _btn(
|
||||||
|
IconData icon,
|
||||||
|
String text,
|
||||||
|
Color fg,
|
||||||
|
Color bg,
|
||||||
|
VoidCallback onTap,
|
||||||
|
double scale,
|
||||||
|
) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
constraints: BoxConstraints(
|
||||||
decoration: BoxDecoration(
|
minWidth: 115 * scale, // 👈 makes button wider
|
||||||
borderRadius: BorderRadius.circular(6),
|
minHeight: 36 * scale, // 👈 makes button taller
|
||||||
color: Colors.indigo.shade50,
|
),
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 16 * scale, // slightly more horizontal space
|
||||||
|
vertical: 8 * scale,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: bg,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center, // 👈 centers content
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 16 * scale, color: fg),
|
||||||
|
SizedBox(width: 6 * scale),
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(
|
||||||
|
color: fg,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 12 * scale,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
child: Text(text, style: const TextStyle(color: Colors.indigo)),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _openOrderDetails(String id) {
|
|
||||||
Navigator.push(context, MaterialPageRoute(
|
// ================= STATUS BADGE =================
|
||||||
builder: (_) => OrderDetailScreen(orderId: id)));
|
|
||||||
|
Widget getStatusBadge(String? status, double scale) {
|
||||||
|
final config = statusConfig(status);
|
||||||
|
final isDomestic = (status ?? '').toLowerCase().contains("domestic");
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: config.bg,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
isDomestic ? "Domestic\nDistribution" : formatStatusLabel(status),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: isDomestic ? 2 : 1,
|
||||||
|
style: TextStyle(
|
||||||
|
color: config.fg,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 11,
|
||||||
|
height: 1.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _openShipment(String id) {
|
void _openOrderDetails(String id) =>
|
||||||
Navigator.push(context, MaterialPageRoute(
|
Navigator.push(context,
|
||||||
builder: (_) => OrderShipmentScreen(orderId: id)));
|
MaterialPageRoute(builder: (_) => OrderDetailScreen(orderId: id)));
|
||||||
}
|
|
||||||
|
|
||||||
void _openInvoice(String id) {
|
void _openInvoice(String id) =>
|
||||||
Navigator.push(context, MaterialPageRoute(
|
Navigator.push(context,
|
||||||
builder: (_) => OrderInvoiceScreen(orderId: id)));
|
MaterialPageRoute(builder: (_) => OrderInvoiceScreen(orderId: id)));
|
||||||
}
|
|
||||||
|
|
||||||
void _openTrack(String id) {
|
void _openTrack(String id) =>
|
||||||
Navigator.push(context, MaterialPageRoute(
|
Navigator.push(context,
|
||||||
builder: (_) => OrderTrackScreen(orderId: id)));
|
MaterialPageRoute(builder: (_) => OrderTrackScreen(orderId: id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= PROGRESS BAR =================
|
||||||
|
|
||||||
|
class _AnimatedProgressBar extends StatelessWidget {
|
||||||
|
final double progress;
|
||||||
|
final double scale;
|
||||||
|
|
||||||
|
const _AnimatedProgressBar({required this.progress, required this.scale});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(builder: (context, c) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 8, // 👈 thinner bar
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 600),
|
||||||
|
height: 8,
|
||||||
|
width: c.maxWidth * progress,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: [Color(0xFF4F8CFF), Color(0xFF8A4DFF)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================= STATUS LOGIC =================
|
||||||
|
|
||||||
|
double getProgress(String? status) {
|
||||||
|
final s = (status ?? '').toLowerCase();
|
||||||
|
if (s.contains("shipment ready")) return 0.05;
|
||||||
|
if (s.contains("export")) return 0.10;
|
||||||
|
if (s.contains("international")) return 0.20;
|
||||||
|
if (s.contains("arrived")) return 0.30;
|
||||||
|
if (s.contains("import")) return 0.40;
|
||||||
|
if (s.contains("warehouse")) return 0.50;
|
||||||
|
if (s.contains("domestic")) return 0.60;
|
||||||
|
if (s.contains("out for")) return 0.90;
|
||||||
|
if (s.contains("delivered")) return 1.0;
|
||||||
|
return 0.05;
|
||||||
|
}
|
||||||
|
|
||||||
|
_StatusConfig statusConfig(String? status) {
|
||||||
|
final s = (status ?? '').toLowerCase();
|
||||||
|
|
||||||
|
if (s.contains("shipment ready")) {
|
||||||
|
return _StatusConfig(Colors.blue.shade800, Colors.blue.shade50);
|
||||||
|
}
|
||||||
|
if (s.contains("export")) {
|
||||||
|
return _StatusConfig(Colors.purple.shade800, Colors.purple.shade50);
|
||||||
|
}
|
||||||
|
if (s.contains("international")) {
|
||||||
|
return _StatusConfig(Colors.red.shade800, Colors.red.shade50);
|
||||||
|
}
|
||||||
|
if (s.contains("arrived")) {
|
||||||
|
return _StatusConfig(Colors.orange.shade800, Colors.orange.shade50);
|
||||||
|
}
|
||||||
|
if (s.contains("import")) {
|
||||||
|
return _StatusConfig(Colors.teal.shade800, Colors.teal.shade50);
|
||||||
|
}
|
||||||
|
if (s.contains("warehouse")) {
|
||||||
|
return _StatusConfig(Colors.brown.shade800, Colors.brown.shade50);
|
||||||
|
}
|
||||||
|
if (s.contains("domestic")) {
|
||||||
|
return _StatusConfig(Colors.indigo.shade800, Colors.indigo.shade50);
|
||||||
|
}
|
||||||
|
if (s.contains("out for")) {
|
||||||
|
return _StatusConfig(Colors.pink.shade800, Colors.pink.shade50);
|
||||||
|
}
|
||||||
|
if (s.contains("delivered")) {
|
||||||
|
return _StatusConfig(Colors.green.shade800, Colors.green.shade50);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _StatusConfig(Colors.grey.shade800, Colors.grey.shade300);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StatusConfig {
|
||||||
|
final Color fg;
|
||||||
|
final Color bg;
|
||||||
|
_StatusConfig(this.fg, this.bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= STATUS LABEL MAP =================
|
||||||
|
|
||||||
|
const Map<String, String> statusLabelMap = {
|
||||||
|
'shipment_ready': 'Shipment Ready',
|
||||||
|
'export_custom': 'Export Custom',
|
||||||
|
'international_transit': 'International Transit',
|
||||||
|
'arrived_at_port': 'Arrived At Port',
|
||||||
|
'import_custom': 'Import Custom',
|
||||||
|
'warehouse': 'Warehouse Processing',
|
||||||
|
'domestic_distribution': 'Domestic Distribution',
|
||||||
|
'out_for_delivery': 'Out For Delivery',
|
||||||
|
'delivered': 'Delivered',
|
||||||
|
};
|
||||||
|
|
||||||
|
String formatStatusLabel(String? status) {
|
||||||
|
if (status == null || status.isEmpty) return "Unknown";
|
||||||
|
|
||||||
|
return statusLabelMap[status.toLowerCase()] ??
|
||||||
|
status.replaceAll('_', ' ').toUpperCase();
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
// lib/screens/order_track_screen.dart
|
||||||
|
import 'dart:math';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../services/dio_client.dart';
|
import '../services/dio_client.dart';
|
||||||
import '../services/order_service.dart';
|
import '../services/order_service.dart';
|
||||||
|
import 'order_screen.dart';
|
||||||
|
|
||||||
class OrderTrackScreen extends StatefulWidget {
|
class OrderTrackScreen extends StatefulWidget {
|
||||||
final String orderId;
|
final String orderId;
|
||||||
@@ -11,54 +13,675 @@ class OrderTrackScreen extends StatefulWidget {
|
|||||||
State<OrderTrackScreen> createState() => _OrderTrackScreenState();
|
State<OrderTrackScreen> createState() => _OrderTrackScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _OrderTrackScreenState extends State<OrderTrackScreen> {
|
class _OrderTrackScreenState extends State<OrderTrackScreen>
|
||||||
|
with TickerProviderStateMixin {
|
||||||
bool loading = true;
|
bool loading = true;
|
||||||
Map data = {};
|
|
||||||
|
Map<String, dynamic>? shipment;
|
||||||
|
Map<String, dynamic> trackData = {};
|
||||||
|
|
||||||
|
late final AnimationController progressController;
|
||||||
|
late final AnimationController shipController;
|
||||||
|
late final AnimationController timelineController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
load();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> load() async {
|
progressController = AnimationController(
|
||||||
final service = OrderService(DioClient.getInstance(context));
|
vsync: this,
|
||||||
final res = await service.trackOrder(widget.orderId);
|
duration: const Duration(milliseconds: 900),
|
||||||
|
);
|
||||||
|
|
||||||
if (res['success'] == true) {
|
shipController = AnimationController(
|
||||||
data = res['track'];
|
vsync: this,
|
||||||
}
|
duration: const Duration(milliseconds: 1600),
|
||||||
|
)..repeat(reverse: true);
|
||||||
|
|
||||||
loading = false;
|
timelineController = AnimationController(
|
||||||
setState(() {});
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 1200),
|
||||||
|
);
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _loadData());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
progressController.dispose();
|
||||||
|
shipController.dispose();
|
||||||
|
timelineController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- LOAD DATA ----------------
|
||||||
|
Future<void> _loadData() async {
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
setState(() => loading = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final service = OrderService(DioClient.getInstance(context));
|
||||||
|
|
||||||
|
final results = await Future.wait([
|
||||||
|
service.getShipment(widget.orderId)
|
||||||
|
.catchError((_) => {"success": false}),
|
||||||
|
service.trackOrder(widget.orderId)
|
||||||
|
.catchError((_) => {"success": false}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
final shipRes = results[0] as Map;
|
||||||
|
final trackRes = results[1] as Map;
|
||||||
|
|
||||||
|
shipment = shipRes["success"] == true
|
||||||
|
? Map<String, dynamic>.from(shipRes["shipment"] ?? {})
|
||||||
|
: null;
|
||||||
|
|
||||||
|
trackData = trackRes["success"] == true
|
||||||
|
? Map<String, dynamic>.from(trackRes["track"] ?? {})
|
||||||
|
: {};
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
final target = _computeProgress();
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
try {
|
||||||
|
await progressController.animateTo(
|
||||||
|
target,
|
||||||
|
curve: Curves.easeInOutCubic,
|
||||||
|
);
|
||||||
|
// Animate timeline
|
||||||
|
timelineController.forward(from: 0);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) setState(() => loading = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- PROGRESS LOGIC ----------------
|
||||||
|
double _computeProgress() {
|
||||||
|
final status =
|
||||||
|
(trackData["shipment_status"] ?? "").toString().toLowerCase();
|
||||||
|
|
||||||
|
if (status.contains("delivered")) return 1.0;
|
||||||
|
if (status.contains("dispatched")) return 0.85;
|
||||||
|
if (status.contains("transit")) return 0.65;
|
||||||
|
if (status.contains("loading")) return 0.40;
|
||||||
|
if (status.contains("pending")) return 0.25;
|
||||||
|
|
||||||
|
return 0.05;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _fmt(dynamic v) {
|
||||||
|
if (v == null) return "-";
|
||||||
|
try {
|
||||||
|
final d = DateTime.parse(v.toString()).toLocal();
|
||||||
|
return "${d.day}/${d.month}/${d.year}";
|
||||||
|
} catch (_) {
|
||||||
|
return v.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- SHIPMENT STEPS DATA ----------------
|
||||||
|
final List<Map<String, dynamic>> _shipmentSteps = [
|
||||||
|
{
|
||||||
|
'title': 'Shipment Ready',
|
||||||
|
'status_key': 'shipment_ready',
|
||||||
|
'icon': Icons.inventory,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Export Custom',
|
||||||
|
'status_key': 'export_custom',
|
||||||
|
'icon': Icons.account_balance,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'International Transit',
|
||||||
|
'status_key': 'international_transit',
|
||||||
|
'icon': Icons.flight,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Arrived at India',
|
||||||
|
'status_key': 'arrived_india',
|
||||||
|
'icon': Icons.flag,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Import Custom',
|
||||||
|
'status_key': 'import_custom',
|
||||||
|
'icon': Icons.account_balance_outlined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Warehouse',
|
||||||
|
'status_key': 'warehouse',
|
||||||
|
'icon': Icons.warehouse,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Domestic Distribution',
|
||||||
|
'status_key': 'domestic_distribution',
|
||||||
|
'icon': Icons.local_shipping,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Out for Delivery',
|
||||||
|
'status_key': 'out_for_delivery',
|
||||||
|
'icon': Icons.delivery_dining,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Delivered',
|
||||||
|
'status_key': 'delivered',
|
||||||
|
'icon': Icons.verified,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ---------------- STATUS MAPPING ----------------
|
||||||
|
Map<String, int> _statusToIndex = {
|
||||||
|
'shipment_ready': 0,
|
||||||
|
'export_custom': 1,
|
||||||
|
'international_transit': 2,
|
||||||
|
'arrived_india': 3,
|
||||||
|
'import_custom': 4,
|
||||||
|
'warehouse': 5,
|
||||||
|
'domestic_distribution': 6,
|
||||||
|
'out_for_delivery': 7,
|
||||||
|
'delivered': 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
int _getCurrentStepIndex() {
|
||||||
|
final status = (trackData["shipment_status"] ?? "").toString().toLowerCase();
|
||||||
|
|
||||||
|
// Try to match exact status key first
|
||||||
|
for (var entry in _statusToIndex.entries) {
|
||||||
|
if (status.contains(entry.key)) {
|
||||||
|
return entry.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback mappings
|
||||||
|
if (status.contains("delivered")) return 8;
|
||||||
|
if (status.contains("dispatch") || status.contains("out for delivery")) return 7;
|
||||||
|
if (status.contains("distribution")) return 6;
|
||||||
|
if (status.contains("warehouse")) return 5;
|
||||||
|
if (status.contains("import")) return 4;
|
||||||
|
if (status.contains("arrived") || status.contains("india")) return 3;
|
||||||
|
if (status.contains("transit")) return 2;
|
||||||
|
if (status.contains("export")) return 1;
|
||||||
|
if (status.contains("ready") || status.contains("pending")) return 0;
|
||||||
|
|
||||||
|
return 0; // Default to first step
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- UI BUILD ----------------
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final width = MediaQuery.of(context).size.width;
|
||||||
|
final scale = (width / 430).clamp(0.75, 1.25);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text("Track Order")),
|
appBar: AppBar(
|
||||||
|
title: const Text("Shipment & Tracking"),
|
||||||
|
elevation: 0.8,
|
||||||
|
),
|
||||||
body: loading
|
body: loading
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: Padding(
|
: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: EdgeInsets.all(16 * scale),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text("Order ID: ${data['order_id']}"),
|
_headerCard(scale),
|
||||||
Text("Shipment Status: ${data['shipment_status']}"),
|
SizedBox(height: 16 * scale),
|
||||||
Text("Shipment Date: ${data['shipment_date']}"),
|
|
||||||
|
|
||||||
const SizedBox(height: 20),
|
_shipmentSummary(scale),
|
||||||
Center(
|
SizedBox(height: 20 * scale),
|
||||||
child: Icon(
|
|
||||||
Icons.local_shipping,
|
_trackingStatus(scale),
|
||||||
size: 100,
|
SizedBox(height: 16 * scale),
|
||||||
color: Colors.indigo.shade300,
|
|
||||||
),
|
// Replace horizontal progress bar with vertical timeline
|
||||||
),
|
_shipmentTimeline(scale),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------- HEADER ----------------
|
||||||
|
Widget _headerCard(double scale) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(14 * scale),
|
||||||
|
decoration: _boxDecoration(scale),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(10 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(12 * scale),
|
||||||
|
),
|
||||||
|
child: Icon(Icons.local_shipping,
|
||||||
|
color: Colors.blue, size: 28 * scale),
|
||||||
|
),
|
||||||
|
SizedBox(width: 12 * scale),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Order #${trackData['order_id'] ?? '-'}",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, fontSize: 16 * scale),
|
||||||
|
),
|
||||||
|
SizedBox(height: 6 * scale),
|
||||||
|
Text(
|
||||||
|
trackData["shipment_status"] ?? "-",
|
||||||
|
style: TextStyle(color: Colors.black54, fontSize: 13 * scale),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- SHIPMENT SUMMARY ----------------
|
||||||
|
Widget _shipmentSummary(double scale) {
|
||||||
|
if (shipment == null) {
|
||||||
|
return _simpleCard(scale, "Shipment not created yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(16 * scale),
|
||||||
|
decoration: _boxDecoration(scale),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text("Shipment Summary",
|
||||||
|
style:
|
||||||
|
TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
|
||||||
|
SizedBox(height: 12 * scale),
|
||||||
|
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(12 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(12 * scale),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.qr_code_2, color: Colors.blue, size: 22 * scale),
|
||||||
|
SizedBox(width: 10 * scale),
|
||||||
|
Text("Shipment ID: ${shipment!['shipment_id']}",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, fontSize: 15 * scale)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 14 * scale),
|
||||||
|
_twoCol(
|
||||||
|
"Status",
|
||||||
|
formatStatusLabel(shipment!['status']),
|
||||||
|
scale,
|
||||||
|
),
|
||||||
|
|
||||||
|
_twoCol("Shipment Date", _fmt(shipment!['shipment_date']), scale),
|
||||||
|
_twoCol("Origin", shipment!['origin'], scale),
|
||||||
|
_twoCol("Destination", shipment!['destination'], scale),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _simpleCard(double scale, String text) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(14 * scale),
|
||||||
|
decoration: _boxDecoration(scale),
|
||||||
|
child: Text(text,
|
||||||
|
style: TextStyle(fontSize: 15 * scale, color: Colors.grey.shade700)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _twoCol(String title, dynamic value, double scale) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 6 * scale),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(title,
|
||||||
|
style: TextStyle(color: Colors.grey, fontSize: 13 * scale)),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
value?.toString() ?? "-",
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style:
|
||||||
|
TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * scale),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- TRACKING STATUS ----------------
|
||||||
|
Widget _trackingStatus(double scale) {
|
||||||
|
final delivered = _computeProgress() >= 1.0;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding:
|
||||||
|
EdgeInsets.symmetric(vertical: 10 * scale, horizontal: 14 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: delivered
|
||||||
|
? const LinearGradient(colors: [Colors.green, Colors.lightGreen])
|
||||||
|
: const LinearGradient(colors: [Colors.blue, Colors.purple]),
|
||||||
|
borderRadius: BorderRadius.circular(40 * scale),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
delivered ? Icons.verified : Icons.local_shipping,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 16 * scale,
|
||||||
|
),
|
||||||
|
SizedBox(width: 8 * scale),
|
||||||
|
Text(
|
||||||
|
formatStatusLabel(trackData["shipment_status"]),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 13 * scale,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- VERTICAL SHIPMENT TIMELINE ----------------
|
||||||
|
Widget _shipmentTimeline(double scale) {
|
||||||
|
final currentStepIndex = _getCurrentStepIndex();
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(16 * scale),
|
||||||
|
decoration: _boxDecoration(scale),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.timeline, color: Colors.blue, size: 20 * scale),
|
||||||
|
SizedBox(width: 8 * scale),
|
||||||
|
Text(
|
||||||
|
"Shipment Progress",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 17 * scale,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 8 * scale),
|
||||||
|
Text(
|
||||||
|
"Tracking your package in real-time",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
fontSize: 13 * scale,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 16 * scale),
|
||||||
|
|
||||||
|
// Timeline Container
|
||||||
|
AnimatedBuilder(
|
||||||
|
animation: timelineController,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Opacity(
|
||||||
|
opacity: timelineController.value,
|
||||||
|
child: Transform.translate(
|
||||||
|
offset: Offset(0, 20 * (1 - timelineController.value)),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: _buildTimeline(scale, currentStepIndex),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTimeline(double scale, int currentStepIndex) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.only(left: 8 * scale),
|
||||||
|
child: ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: _shipmentSteps.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final step = _shipmentSteps[index];
|
||||||
|
final isCompleted = index < currentStepIndex;
|
||||||
|
final isCurrent = index == currentStepIndex;
|
||||||
|
final isLast = index == _shipmentSteps.length - 1;
|
||||||
|
|
||||||
|
return _buildTimelineStep(
|
||||||
|
scale: scale,
|
||||||
|
step: step,
|
||||||
|
index: index,
|
||||||
|
isCompleted: isCompleted,
|
||||||
|
isCurrent: isCurrent,
|
||||||
|
isLast: isLast,
|
||||||
|
currentStepIndex: currentStepIndex,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTimelineStep({
|
||||||
|
required double scale,
|
||||||
|
required Map<String, dynamic> step,
|
||||||
|
required int index,
|
||||||
|
required bool isCompleted,
|
||||||
|
required bool isCurrent,
|
||||||
|
required bool isLast,
|
||||||
|
required int currentStepIndex,
|
||||||
|
}) {
|
||||||
|
return IntrinsicHeight(
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Timeline Column (Line + Icon)
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
// Top connector line (except for first item)
|
||||||
|
if (index > 0)
|
||||||
|
Container(
|
||||||
|
width: 2 * scale,
|
||||||
|
height: 24 * scale,
|
||||||
|
color: isCompleted ? Colors.green : Colors.grey.shade300,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Step Icon with animation
|
||||||
|
_buildStepIcon(scale, isCompleted, isCurrent, step['icon']),
|
||||||
|
|
||||||
|
// Bottom connector line (except for last item)
|
||||||
|
if (!isLast)
|
||||||
|
Container(
|
||||||
|
width: 2 * scale,
|
||||||
|
height: 24 * scale,
|
||||||
|
color: index < currentStepIndex ? Colors.green : Colors.grey.shade300,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(width: 16 * scale),
|
||||||
|
|
||||||
|
// Step Content
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
margin: EdgeInsets.only(bottom: 20 * scale),
|
||||||
|
padding: EdgeInsets.all(14 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isCurrent ? Colors.blue.shade50 : Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(12 * scale),
|
||||||
|
border: isCurrent
|
||||||
|
? Border.all(color: Colors.blue.shade200, width: 1.5 * scale)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
step['title'],
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15 * scale,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: isCurrent
|
||||||
|
? Colors.blue.shade800
|
||||||
|
: isCompleted
|
||||||
|
? Colors.green.shade800
|
||||||
|
: Colors.grey.shade800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isCompleted)
|
||||||
|
Icon(Icons.check_circle,
|
||||||
|
color: Colors.green,
|
||||||
|
size: 18 * scale
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 4 * scale),
|
||||||
|
|
||||||
|
// Optional timestamp - you can customize this with actual timestamps from API
|
||||||
|
_buildStepTimestamp(scale, step, isCompleted, isCurrent, index),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStepIcon(double scale, bool isCompleted, bool isCurrent, IconData iconData) {
|
||||||
|
// Animation for current step
|
||||||
|
if (isCurrent) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: shipController,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Container(
|
||||||
|
width: 32 * scale * (1 + 0.2 * shipController.value),
|
||||||
|
height: 32 * scale * (1 + 0.2 * shipController.value),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: Colors.blue.shade50,
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.blue,
|
||||||
|
width: 2 * scale,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
iconData,
|
||||||
|
color: Colors.blue,
|
||||||
|
size: 18 * scale,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Completed step
|
||||||
|
if (isCompleted) {
|
||||||
|
return Container(
|
||||||
|
width: 32 * scale,
|
||||||
|
height: 32 * scale,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: Colors.green.shade50,
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.green,
|
||||||
|
width: 2 * scale,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.check,
|
||||||
|
color: Colors.green,
|
||||||
|
size: 18 * scale,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upcoming step
|
||||||
|
return Container(
|
||||||
|
width: 32 * scale,
|
||||||
|
height: 32 * scale,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.grey.shade400,
|
||||||
|
width: 2 * scale,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
iconData,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
size: 18 * scale,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStepTimestamp(double scale, Map<String, dynamic> step,
|
||||||
|
bool isCompleted, bool isCurrent, int index) {
|
||||||
|
// You can replace this with actual timestamps from your API
|
||||||
|
final now = DateTime.now();
|
||||||
|
final estimatedTime = now.add(Duration(days: index));
|
||||||
|
|
||||||
|
String statusText = isCurrent
|
||||||
|
? "In progress"
|
||||||
|
: isCompleted
|
||||||
|
? "Completed"
|
||||||
|
: "Estimated ${estimatedTime.day}/${estimatedTime.month}";
|
||||||
|
|
||||||
|
Color statusColor = isCurrent
|
||||||
|
? Colors.blue.shade600
|
||||||
|
: isCompleted
|
||||||
|
? Colors.green.shade600
|
||||||
|
: Colors.grey.shade600;
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
isCurrent ? Icons.access_time : Icons.calendar_today,
|
||||||
|
color: statusColor,
|
||||||
|
size: 14 * scale,
|
||||||
|
),
|
||||||
|
SizedBox(width: 4 * scale),
|
||||||
|
Text(
|
||||||
|
statusText,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12 * scale,
|
||||||
|
color: statusColor,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
BoxDecoration _boxDecoration(double scale) {
|
||||||
|
return BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12 * scale),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black12.withOpacity(0.05),
|
||||||
|
blurRadius: 10 * scale,
|
||||||
|
offset: Offset(0, 4 * scale),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -16,52 +16,124 @@ class _OtpScreenState extends State<OtpScreen> {
|
|||||||
final otpController = TextEditingController();
|
final otpController = TextEditingController();
|
||||||
bool verifying = false;
|
bool verifying = false;
|
||||||
|
|
||||||
static const String defaultOtp = '123456'; // default OTP as you said
|
static const String defaultOtp = '123456';
|
||||||
|
|
||||||
void _verifyAndSubmit() async {
|
void _verifyAndSubmit() async {
|
||||||
final entered = otpController.text.trim();
|
final entered = otpController.text.trim();
|
||||||
|
|
||||||
if (entered.length != 6) {
|
if (entered.length != 6) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Enter 6 digit OTP')));
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(const SnackBar(content: Text('Enter 6 digit OTP')));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entered != defaultOtp) {
|
if (entered != defaultOtp) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Invalid OTP')));
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(const SnackBar(content: Text('Invalid OTP')));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() => verifying = true);
|
setState(() => verifying = true);
|
||||||
|
|
||||||
// send signup payload to backend
|
|
||||||
final res = await RequestService(context).sendSignup(widget.signupPayload);
|
final res = await RequestService(context).sendSignup(widget.signupPayload);
|
||||||
|
|
||||||
setState(() => verifying = false);
|
setState(() => verifying = false);
|
||||||
|
|
||||||
if (res['status'] == true || res['status'] == 'success') {
|
if (res['status'] == true || res['status'] == 'success') {
|
||||||
// navigate to waiting screen
|
Navigator.of(context).pushReplacement(
|
||||||
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const WaitingScreen()));
|
MaterialPageRoute(builder: (_) => const WaitingScreen()));
|
||||||
} else {
|
} else {
|
||||||
final message = res['message']?.toString() ?? 'Failed';
|
final message = res['message']?.toString() ?? 'Failed';
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(SnackBar(content: Text(message)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final pad = MediaQuery.of(context).size.width * 0.06;
|
final width = MediaQuery.of(context).size.width;
|
||||||
|
|
||||||
|
/// 📌 Universal scale factor for responsiveness
|
||||||
|
final scale = (width / 390).clamp(0.85, 1.25);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text("OTP Verification")),
|
body: SafeArea(
|
||||||
body: Padding(
|
child: Stack(
|
||||||
padding: EdgeInsets.symmetric(horizontal: pad, vertical: 20),
|
children: [
|
||||||
child: Column(children: [
|
/// 🔙 Back Button
|
||||||
const Text("Enter the 6-digit OTP sent to your mobile/email. (Default OTP: 123456)"),
|
Positioned(
|
||||||
const SizedBox(height: 20),
|
top: 18 * scale,
|
||||||
RoundedInput(controller: otpController, hint: "Enter OTP", keyboardType: TextInputType.number),
|
left: 18 * scale,
|
||||||
const SizedBox(height: 14),
|
child: GestureDetector(
|
||||||
PrimaryButton(label: "Verify & Submit", onTap: _verifyAndSubmit, busy: verifying),
|
onTap: () => Navigator.pop(context),
|
||||||
]),
|
child: Container(
|
||||||
|
height: 42 * scale,
|
||||||
|
width: 42 * scale,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.indigo,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(Icons.arrow_back,
|
||||||
|
color: Colors.white, size: 22 * scale),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
/// 🟦 Center Card
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
width: width * 0.90,
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 20 * scale, vertical: 28 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16 * scale),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.08),
|
||||||
|
blurRadius: 12 * scale,
|
||||||
|
offset: Offset(0, 4 * scale),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"OTP Verification",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 22 * scale,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 10 * scale),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"Enter the 6-digit OTP sent to your mobile/email.\n(Default OTP: 123456)",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 13.5 * scale),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 18 * scale),
|
||||||
|
|
||||||
|
RoundedInput(
|
||||||
|
controller: otpController,
|
||||||
|
hint: "Enter OTP",
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 22 * scale),
|
||||||
|
|
||||||
|
PrimaryButton(
|
||||||
|
label: "Verify & Submit",
|
||||||
|
onTap: _verifyAndSubmit,
|
||||||
|
busy: verifying,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,13 +9,11 @@ import 'login_screen.dart';
|
|||||||
|
|
||||||
class SettingsScreen extends StatefulWidget {
|
class SettingsScreen extends StatefulWidget {
|
||||||
const SettingsScreen({super.key});
|
const SettingsScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SettingsScreen> createState() => _SettingsScreenState();
|
State<SettingsScreen> createState() => _SettingsScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SettingsScreenState extends State<SettingsScreen> {
|
class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -27,33 +25,34 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
await Future.delayed(const Duration(milliseconds: 100));
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
final profileProvider =
|
final profile = Provider.of<UserProfileProvider>(context, listen: false);
|
||||||
Provider.of<UserProfileProvider>(context, listen: false);
|
profile.init(context);
|
||||||
profileProvider.init(context);
|
await profile.loadProfile(context);
|
||||||
|
|
||||||
await profileProvider.loadProfile(context);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _pickImage() async {
|
Future<void> _pickImage() async {
|
||||||
|
try {
|
||||||
final picked = await ImagePicker().pickImage(
|
final picked = await ImagePicker().pickImage(
|
||||||
source: ImageSource.gallery,
|
source: ImageSource.gallery,
|
||||||
imageQuality: 80,
|
imageQuality: 80,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (picked != null) {
|
if (picked != null) {
|
||||||
final file = File(picked.path);
|
final file = File(picked.path);
|
||||||
final profileProvider =
|
final profile = Provider.of<UserProfileProvider>(context, listen: false);
|
||||||
Provider.of<UserProfileProvider>(context, listen: false);
|
profile.init(context);
|
||||||
|
final ok = await profile.updateProfileImage(context, file);
|
||||||
profileProvider.init(context);
|
|
||||||
|
|
||||||
final success = await profileProvider.updateProfileImage(context, file);
|
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(success ? "Profile updated" : "Failed to update")),
|
SnackBar(content: Text(ok ? "Profile updated" : "Failed to update")),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(SnackBar(content: Text("Error: $e")));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _logout() async {
|
Future<void> _logout() async {
|
||||||
@@ -62,131 +61,309 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
final confirm = await showDialog<bool>(
|
final confirm = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) => AlertDialog(
|
builder: (_) => AlertDialog(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||||
title: const Text("Logout"),
|
title: const Text("Logout"),
|
||||||
content: const Text("Are you sure you want to logout?"),
|
content: const Text("Are you sure you want to logout?"),
|
||||||
actions: [
|
actions: [
|
||||||
|
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("Cancel")),
|
||||||
TextButton(
|
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),
|
onPressed: () => Navigator.pop(context, true),
|
||||||
),
|
child: const Text("Logout", style: TextStyle(color: Colors.red))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (confirm == true) {
|
if (confirm == true) {
|
||||||
await auth.logout(context);
|
await auth.logout(context);
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
Navigator.pushAndRemoveUntil(
|
Navigator.pushAndRemoveUntil(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (_) => const LoginScreen()),
|
MaterialPageRoute(builder: (_) => const LoginScreen()),
|
||||||
(route) => false,
|
(r) => false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
// ------------------------- REUSABLE FIELD ROW -------------------------
|
||||||
Widget build(BuildContext context) {
|
Widget _fieldRow(IconData icon, String label, String value, double scale) {
|
||||||
final profileProvider = Provider.of<UserProfileProvider>(context);
|
return Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 12 * scale),
|
||||||
if (profileProvider.loading || profileProvider.profile == null) {
|
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
return const Center(child: CircularProgressIndicator());
|
Icon(icon, size: 26 * scale, color: Colors.blueGrey.shade700),
|
||||||
|
SizedBox(width: 14 * scale),
|
||||||
|
Expanded(
|
||||||
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text(label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14 * scale,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
fontWeight: FontWeight.w700)),
|
||||||
|
SizedBox(height: 4 * scale),
|
||||||
|
Text(value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16 * scale, fontWeight: FontWeight.bold)),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final p = profileProvider.profile!;
|
// ------------------------- INFO TILE -------------------------
|
||||||
|
Widget _infoTile(IconData icon, String title, String value, double scale) {
|
||||||
return SingleChildScrollView(
|
return Row(
|
||||||
padding: const EdgeInsets.all(18),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// ------------------ PROFILE IMAGE ------------------
|
Icon(icon, size: 26 * scale, color: Colors.orange.shade800),
|
||||||
Center(
|
SizedBox(width: 14 * scale),
|
||||||
child: GestureDetector(
|
Expanded(
|
||||||
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(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(title,
|
Text(title,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13 * scale,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Colors.grey)),
|
color: Colors.black54)),
|
||||||
const SizedBox(height: 4),
|
SizedBox(height: 4 * scale),
|
||||||
Text(value, style: const TextStyle(fontSize: 16)),
|
Text(value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 17 * scale, fontWeight: FontWeight.bold))
|
||||||
|
]),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final profile = Provider.of<UserProfileProvider>(context);
|
||||||
|
|
||||||
|
if (profile.loading || profile.profile == null) {
|
||||||
|
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------- RESPONSIVE SCALE -----------
|
||||||
|
final width = MediaQuery.of(context).size.width;
|
||||||
|
final scale = (width / 390).clamp(0.80, 1.25);
|
||||||
|
|
||||||
|
final p = profile.profile!;
|
||||||
|
final img = p.profileImage;
|
||||||
|
final name = p.customerName ?? "Unknown";
|
||||||
|
final email = p.email ?? "Not provided";
|
||||||
|
final status = p.status ?? "Active";
|
||||||
|
final cid = p.customerId ?? "—";
|
||||||
|
final company = p.companyName ?? "—";
|
||||||
|
final type = p.customerType ?? "—";
|
||||||
|
final mobile = p.mobile ?? "—";
|
||||||
|
final address = p.address ?? "Not provided";
|
||||||
|
final pincode = p.pincode ?? "—";
|
||||||
|
final isPartner = type.toLowerCase().contains("partner");
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: const Color(0xFFE9F2FF),
|
||||||
|
body: SafeArea(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.all(18 * scale),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// -------------------- PROFILE SECTION --------------------
|
||||||
|
Center(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: _pickImage,
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 64 * scale,
|
||||||
|
backgroundColor: Colors.grey[200],
|
||||||
|
backgroundImage:
|
||||||
|
img != null ? NetworkImage(img) : null,
|
||||||
|
child: img == null
|
||||||
|
? Icon(Icons.person,
|
||||||
|
size: 70 * scale, color: Colors.grey[600])
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
|
||||||
|
// ------------------ FIXED STATUS BADGE ------------------
|
||||||
|
Positioned(
|
||||||
|
bottom: 8 * scale,
|
||||||
|
right: 8 * scale,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 12 * scale,
|
||||||
|
vertical: 6 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: status.toLowerCase() == 'active'
|
||||||
|
? Colors.green
|
||||||
|
: Colors.orange,
|
||||||
|
borderRadius: BorderRadius.circular(20 * scale),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.2),
|
||||||
|
blurRadius: 8 * scale)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
status,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 13 * scale,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 14 * scale),
|
||||||
|
Text(name,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20 * scale,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
|
SizedBox(height: 6 * scale),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.email,
|
||||||
|
size: 18 * scale, color: Colors.blueGrey),
|
||||||
|
SizedBox(width: 8 * scale),
|
||||||
|
Text(email,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14 * scale,
|
||||||
|
color: Colors.grey[700])),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 26 * scale),
|
||||||
|
|
||||||
|
// ---------------------- YELLOW SUMMARY CARD ----------------------
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(18 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: [Color(0xFFFFF8A3), Color(0xFFFFE275)],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(16 * scale),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.orange.withOpacity(0.25),
|
||||||
|
blurRadius: 14 * scale,
|
||||||
|
offset: Offset(0, 8 * scale))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_infoTile(Icons.badge, "Customer ID", cid, scale),
|
||||||
|
Divider(height: 30 * scale),
|
||||||
|
_infoTile(Icons.business, "Company Name", company, scale),
|
||||||
|
Divider(height: 30 * scale),
|
||||||
|
_infoTile(Icons.category, "Customer Type", type, scale),
|
||||||
|
SizedBox(height: 20 * scale),
|
||||||
|
|
||||||
|
if (isPartner)
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(14 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(14 * scale),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.08),
|
||||||
|
blurRadius: 10 * scale)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(children: [
|
||||||
|
Icon(Icons.workspace_premium,
|
||||||
|
size: 32 * scale, color: Colors.amber[800]),
|
||||||
|
SizedBox(width: 12 * scale),
|
||||||
|
Text("Partner",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16 * scale,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 24 * scale),
|
||||||
|
|
||||||
|
// ---------------------- DETAILS CARD ----------------------
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(16 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(14 * scale),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.06),
|
||||||
|
blurRadius: 12 * scale)
|
||||||
|
]),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_fieldRow(Icons.phone_android, "Mobile", mobile, scale),
|
||||||
|
const Divider(),
|
||||||
|
_fieldRow(Icons.location_on, "Address", address, scale),
|
||||||
|
const Divider(),
|
||||||
|
_fieldRow(Icons.local_post_office, "Pincode", pincode, scale),
|
||||||
|
SizedBox(height: 20 * scale),
|
||||||
|
|
||||||
|
Row(children: [
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) =>
|
||||||
|
const EditProfileScreen()));
|
||||||
|
},
|
||||||
|
icon: Icon(Icons.edit,
|
||||||
|
color: Colors.white, size: 18 * scale),
|
||||||
|
label: Text("Edit Profile",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white, fontSize: 14 * scale)),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.blue[700],
|
||||||
|
padding:
|
||||||
|
EdgeInsets.symmetric(vertical: 14 * scale),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12 * scale)),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
SizedBox(width: 12 * scale),
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: _logout,
|
||||||
|
icon: Icon(Icons.logout,
|
||||||
|
size: 18 * scale, color: Colors.white),
|
||||||
|
label: Text("Logout",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white, fontSize: 14 * scale)),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red[600],
|
||||||
|
padding:
|
||||||
|
EdgeInsets.symmetric(vertical: 14 * scale),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12 * scale)),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
])
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 30 * scale),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,16 +21,20 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
bool sending = false;
|
bool sending = false;
|
||||||
|
|
||||||
void _sendOtp() async {
|
void _sendOtp() async {
|
||||||
// We don't call backend for OTP here per your flow - OTP is default 123456.
|
if (cName.text.trim().isEmpty ||
|
||||||
// Validate minimal
|
cCompany.text.trim().isEmpty ||
|
||||||
if (cName.text.trim().isEmpty || cCompany.text.trim().isEmpty || cEmail.text.trim().isEmpty || cMobile.text.trim().isEmpty) {
|
cEmail.text.trim().isEmpty ||
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please fill the required fields')));
|
cMobile.text.trim().isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Please fill the required fields')),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() => sending = true);
|
setState(() => sending = true);
|
||||||
await Future.delayed(const Duration(milliseconds: 600)); // UI feel
|
await Future.delayed(const Duration(milliseconds: 600));
|
||||||
setState(() => sending = false);
|
setState(() => sending = false);
|
||||||
// Navigate to OTP screen with collected data
|
|
||||||
final data = {
|
final data = {
|
||||||
'customer_name': cName.text.trim(),
|
'customer_name': cName.text.trim(),
|
||||||
'company_name': cCompany.text.trim(),
|
'company_name': cCompany.text.trim(),
|
||||||
@@ -40,40 +44,152 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
'address': cAddress.text.trim(),
|
'address': cAddress.text.trim(),
|
||||||
'pincode': cPincode.text.trim(),
|
'pincode': cPincode.text.trim(),
|
||||||
};
|
};
|
||||||
Navigator.of(context).push(MaterialPageRoute(builder: (_) => OtpScreen(signupPayload: data)));
|
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => OtpScreen(signupPayload: data),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final pad = MediaQuery.of(context).size.width * 0.06;
|
final width = MediaQuery.of(context).size.width;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text("Create Account")),
|
backgroundColor: const Color(0xFFE8F0FF), // Same as Login background
|
||||||
|
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: pad),
|
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(children: [
|
child: Padding(
|
||||||
const SizedBox(height: 16),
|
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16),
|
||||||
RoundedInput(controller: cName, hint: "Customer name"),
|
|
||||||
const SizedBox(height: 12),
|
child: Column(
|
||||||
RoundedInput(controller: cCompany, hint: "Company name"),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
const SizedBox(height: 12),
|
children: [
|
||||||
RoundedInput(controller: cDesignation, hint: "Designation (optional)"),
|
/// 🔵 Back Button (scrolls with form)
|
||||||
const SizedBox(height: 12),
|
Material(
|
||||||
RoundedInput(controller: cEmail, hint: "Email", keyboardType: TextInputType.emailAddress),
|
elevation: 6,
|
||||||
const SizedBox(height: 12),
|
shape: const CircleBorder(),
|
||||||
RoundedInput(controller: cMobile, hint: "Mobile", keyboardType: TextInputType.phone),
|
color: Colors.indigo.shade700,
|
||||||
const SizedBox(height: 12),
|
child: InkWell(
|
||||||
RoundedInput(controller: cAddress, hint: "Address", maxLines: 3),
|
borderRadius: BorderRadius.circular(30),
|
||||||
const SizedBox(height: 12),
|
onTap: () => Navigator.pop(context),
|
||||||
RoundedInput(controller: cPincode, hint: "Pincode", keyboardType: TextInputType.number),
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.all(10),
|
||||||
|
child: Icon(Icons.arrow_back, color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
PrimaryButton(label: "Send OTP", onTap: _sendOtp, busy: sending),
|
|
||||||
|
/// 📦 White Elevated Signup Box
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
width: width * 0.88,
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 28, horizontal: 22),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(22),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black12,
|
||||||
|
blurRadius: 18,
|
||||||
|
spreadRadius: 2,
|
||||||
|
offset: const Offset(0, 6),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Create Account",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 26,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.indigo.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 25),
|
||||||
|
|
||||||
|
_blueInput(cName, "Customer name"),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
]),
|
|
||||||
|
_blueInput(cCompany, "Company name"),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
|
||||||
|
_blueInput(cDesignation, "Designation (optional)"),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
|
||||||
|
_blueInput(
|
||||||
|
cEmail,
|
||||||
|
"Email",
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
|
||||||
|
_blueInput(
|
||||||
|
cMobile,
|
||||||
|
"Mobile",
|
||||||
|
keyboardType: TextInputType.phone,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
|
||||||
|
_blueInput(
|
||||||
|
cAddress,
|
||||||
|
"Address",
|
||||||
|
maxLines: 3,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
|
||||||
|
_blueInput(
|
||||||
|
cPincode,
|
||||||
|
"Pincode",
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 25),
|
||||||
|
|
||||||
|
PrimaryButton(
|
||||||
|
label: "Send OTP",
|
||||||
|
onTap: _sendOtp,
|
||||||
|
busy: sending,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 🔵 Blue soft background input wrapper (same as Login)
|
||||||
|
Widget _blueInput(
|
||||||
|
TextEditingController controller,
|
||||||
|
String hint, {
|
||||||
|
TextInputType? keyboardType,
|
||||||
|
int maxLines = 1,
|
||||||
|
}) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFD8E7FF),
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
child: RoundedInput(
|
||||||
|
controller: controller,
|
||||||
|
hint: hint,
|
||||||
|
// keyboardType: keyboardType,
|
||||||
|
maxLines: maxLines,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
|||||||
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 'dashboard_screen.dart';
|
|
||||||
import 'main_bottom_nav.dart';
|
import 'main_bottom_nav.dart';
|
||||||
import 'welcome_screen.dart';
|
import 'welcome_screen.dart';
|
||||||
|
|
||||||
@@ -13,61 +12,166 @@ class SplashScreen extends StatefulWidget {
|
|||||||
State<SplashScreen> createState() => _SplashScreenState();
|
State<SplashScreen> createState() => _SplashScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SplashScreenState extends State<SplashScreen> {
|
class _SplashScreenState extends State<SplashScreen>
|
||||||
|
with TickerProviderStateMixin {
|
||||||
|
late AnimationController _mainController;
|
||||||
|
late Animation<double> _scaleAnim;
|
||||||
|
late Animation<double> _fadeAnim;
|
||||||
|
|
||||||
|
late AnimationController _floatController;
|
||||||
|
late Animation<double> _floatAnim;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
// MAIN splash animation
|
||||||
|
_mainController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 1200),
|
||||||
|
);
|
||||||
|
|
||||||
|
_scaleAnim = Tween(begin: 0.6, end: 1.0).animate(
|
||||||
|
CurvedAnimation(parent: _mainController, curve: Curves.easeOutBack),
|
||||||
|
);
|
||||||
|
|
||||||
|
_fadeAnim = Tween(begin: 0.0, end: 1.0).animate(
|
||||||
|
CurvedAnimation(parent: _mainController, curve: Curves.easeIn),
|
||||||
|
);
|
||||||
|
|
||||||
|
// FLOATING animation (infinite)
|
||||||
|
_floatController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
)..repeat(reverse: true);
|
||||||
|
|
||||||
|
_floatAnim = Tween<double>(begin: -10, end: 10).animate(
|
||||||
|
CurvedAnimation(parent: _floatController, curve: Curves.easeInOut),
|
||||||
|
);
|
||||||
|
|
||||||
|
_mainController.forward();
|
||||||
_init();
|
_init();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _init() async {
|
Future<void> _init() async {
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
await Future.delayed(const Duration(milliseconds: 700));
|
||||||
|
|
||||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||||
|
|
||||||
// 🟢 IMPORTANT → WAIT FOR PREFERENCES TO LOAD
|
|
||||||
await auth.init();
|
await auth.init();
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
if (auth.isLoggedIn) {
|
Future.delayed(const Duration(milliseconds: 900), () {
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.pushReplacement(
|
||||||
MaterialPageRoute(builder: (_) => const MainBottomNav()),
|
context,
|
||||||
);
|
MaterialPageRoute(
|
||||||
} else {
|
builder: (_) =>
|
||||||
Navigator.of(context).pushReplacement(
|
auth.isLoggedIn ? const MainBottomNav() : const WelcomeScreen(),
|
||||||
MaterialPageRoute(builder: (_) => const WelcomeScreen()),
|
),
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_mainController.dispose();
|
||||||
|
_floatController.dispose();
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final size = MediaQuery.of(context).size;
|
final width = MediaQuery.of(context).size.width;
|
||||||
|
|
||||||
|
// Responsive scale factor
|
||||||
|
final scale = (width / 430).clamp(0.9, 1.3);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Center(
|
body: Container(
|
||||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
decoration: BoxDecoration(
|
||||||
Container(
|
gradient: LinearGradient(
|
||||||
width: size.width * 0.34,
|
colors: [
|
||||||
height: size.width * 0.34,
|
Colors.blue.shade50,
|
||||||
|
Colors.white,
|
||||||
|
],
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
child: Center(
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: _mainController,
|
||||||
|
builder: (_, __) {
|
||||||
|
return Opacity(
|
||||||
|
opacity: _fadeAnim.value,
|
||||||
|
child: Transform.translate(
|
||||||
|
offset: Offset(0, _floatAnim.value), // ⭐ Floating animation
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// ⭐ Animated Floating White Circle Logo
|
||||||
|
Transform.scale(
|
||||||
|
scale: _scaleAnim.value,
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: _floatController,
|
||||||
|
builder: (_, __) {
|
||||||
|
return Container(
|
||||||
|
width: width * 0.50 * scale,
|
||||||
|
height: width * 0.50 * scale,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
color: Theme.of(context).primaryColor.withOpacity(0.14),
|
color: Colors.white,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black
|
||||||
|
.withOpacity(0.08 + (_floatAnim.value.abs() / 200)),
|
||||||
|
blurRadius: 25 * scale,
|
||||||
|
spreadRadius: 4 * scale,
|
||||||
|
offset: Offset(0, 8 * scale),
|
||||||
),
|
),
|
||||||
child: Center(
|
],
|
||||||
child: Text(
|
),
|
||||||
"K",
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(28 * scale),
|
||||||
|
child: Image.asset(
|
||||||
|
"assets/Images/K.png",
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 22 * scale),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"Kent Logistics",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 48,
|
fontSize: 22 * scale,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.w700,
|
||||||
color: Theme.of(context).primaryColor),
|
letterSpacing: 1.1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 6 * scale),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"Delivering Excellence",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14 * scale,
|
||||||
|
color: Colors.black54,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 18),
|
|
||||||
const Text("Kent Logistics",
|
|
||||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)),
|
|
||||||
]),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,198 @@
|
|||||||
|
import 'dart:math';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class WaitingScreen extends StatelessWidget {
|
class WaitingScreen extends StatefulWidget {
|
||||||
const WaitingScreen({super.key});
|
const WaitingScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<WaitingScreen> createState() => _WaitingScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WaitingScreenState extends State<WaitingScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _ctrl;
|
||||||
|
late final Animation<double> _anim;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_ctrl = AnimationController(
|
||||||
|
vsync: this, duration: const Duration(milliseconds: 1200));
|
||||||
|
|
||||||
|
_anim = Tween<double>(begin: 0.0, end: pi).animate(
|
||||||
|
CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut));
|
||||||
|
|
||||||
|
_ctrl.repeat();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_ctrl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Matrix4 _buildTransform(double value) {
|
||||||
|
return Matrix4.identity()
|
||||||
|
..setEntry(3, 2, 0.001)
|
||||||
|
..rotateX(value);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final width = MediaQuery.of(context).size.width;
|
||||||
|
|
||||||
|
/// ⭐ Universal scaling factor for responsiveness
|
||||||
|
final scale = (width / 390).clamp(0.85, 1.3);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text("Request Submitted")),
|
body: SafeArea(
|
||||||
body: Padding(
|
child: Stack(
|
||||||
padding: const EdgeInsets.all(18.0),
|
children: [
|
||||||
child: Center(
|
/// BACK BUTTON
|
||||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
Positioned(
|
||||||
Icon(Icons.hourglass_top, size: 72, color: Theme.of(context).primaryColor),
|
top: 12 * scale,
|
||||||
const SizedBox(height: 16),
|
left: 12 * scale,
|
||||||
const Text(
|
child: GestureDetector(
|
||||||
"Signup request submitted successfully.",
|
onTap: () => Navigator.pop(context),
|
||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
child: Container(
|
||||||
|
height: 42 * scale,
|
||||||
|
width: 42 * scale,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Color(0xFF0D47A1),
|
||||||
|
Color(0xFF6A1B9A),
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Icon(Icons.arrow_back,
|
||||||
|
color: Colors.white, size: 20 * scale),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
/// MAIN WHITE BOX
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
width: width * 0.90,
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 20 * scale, vertical: 30 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16 * scale),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.10),
|
||||||
|
blurRadius: 14 * scale,
|
||||||
|
offset: Offset(0, 4 * scale),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
/// FLIPPING ICON
|
||||||
|
SizedBox(
|
||||||
|
height: 92 * scale,
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: _anim,
|
||||||
|
builder: (context, child) {
|
||||||
|
final angle = _anim.value;
|
||||||
|
final isBack = angle > (pi / 2);
|
||||||
|
|
||||||
|
return Transform(
|
||||||
|
transform: _buildTransform(angle),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Transform(
|
||||||
|
transform: isBack
|
||||||
|
? Matrix4.rotationY(pi)
|
||||||
|
: Matrix4.identity(),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Icon(
|
||||||
|
Icons.hourglass_top,
|
||||||
|
size: 72 * scale,
|
||||||
|
color: Colors.indigo,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 16 * scale),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"Request Submitted",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 22 * scale,
|
||||||
|
fontWeight: FontWeight.w600),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
|
||||||
const Text(
|
SizedBox(height: 10 * scale),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"Signup request submitted successfully.",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18 * scale,
|
||||||
|
fontWeight: FontWeight.w500),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 8 * scale),
|
||||||
|
|
||||||
|
Text(
|
||||||
"Please wait up to 24 hours for admin approval. You will receive an email once approved.",
|
"Please wait up to 24 hours for admin approval. You will receive an email once approved.",
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 14 * scale),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
|
||||||
ElevatedButton(
|
SizedBox(height: 24 * scale),
|
||||||
|
|
||||||
|
/// BUTTON WITH GRADIENT WRAPPER
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Color(0xFF0D47A1),
|
||||||
|
Color(0xFF6A1B9A),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(12 * scale),
|
||||||
|
),
|
||||||
|
child: ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
Navigator.of(context)
|
||||||
|
.popUntil((route) => route.isFirst);
|
||||||
},
|
},
|
||||||
child: const Padding(padding: EdgeInsets.symmetric(horizontal: 14, vertical: 12), child: Text("Back to Home")),
|
style: ElevatedButton.styleFrom(
|
||||||
style: ElevatedButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))),
|
backgroundColor: Colors.transparent,
|
||||||
|
shadowColor: Colors.transparent,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 20 * scale,
|
||||||
|
vertical: 14 * scale),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(12 * scale),
|
||||||
),
|
),
|
||||||
]),
|
),
|
||||||
|
child: Text(
|
||||||
|
"Back to Home",
|
||||||
|
style: TextStyle(fontSize: 16 * scale),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,39 +1,188 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'signup_screen.dart';
|
import 'signup_screen.dart';
|
||||||
import 'login_screen.dart';
|
import 'login_screen.dart';
|
||||||
|
import '../widgets/primary_button.dart';
|
||||||
|
|
||||||
class WelcomeScreen extends StatelessWidget {
|
class WelcomeScreen extends StatefulWidget {
|
||||||
const WelcomeScreen({super.key});
|
const WelcomeScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<WelcomeScreen> createState() => _WelcomeScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WelcomeScreenState extends State<WelcomeScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<double> _fade;
|
||||||
|
late Animation<Offset> _slide;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 900),
|
||||||
|
);
|
||||||
|
|
||||||
|
_fade = CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
|
||||||
|
_slide = Tween<Offset>(
|
||||||
|
begin: const Offset(0, -0.2),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: Curves.easeInOutBack,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_controller.forward();
|
||||||
|
|
||||||
|
_controller.addStatusListener((status) {
|
||||||
|
if (status == AnimationStatus.completed) {
|
||||||
|
_controller.repeat(reverse: false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _shinyWelcomeText() {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, child) {
|
||||||
|
double shineX = _controller.value % 1;
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Welcome",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 42,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.indigo.shade700,
|
||||||
|
letterSpacing: 1.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
Positioned.fill(
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: ShaderMask(
|
||||||
|
blendMode: BlendMode.srcATop,
|
||||||
|
shaderCallback: (rect) {
|
||||||
|
final pos = shineX * rect.width;
|
||||||
|
|
||||||
|
return LinearGradient(
|
||||||
|
begin: Alignment.centerLeft,
|
||||||
|
end: Alignment.centerRight,
|
||||||
|
stops: [
|
||||||
|
(pos - 50) / rect.width,
|
||||||
|
pos / rect.width,
|
||||||
|
(pos + 50) / rect.width,
|
||||||
|
],
|
||||||
|
colors: [
|
||||||
|
Colors.transparent,
|
||||||
|
Colors.blueAccent,
|
||||||
|
Colors.transparent,
|
||||||
|
],
|
||||||
|
).createShader(rect);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
"Welcome",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 42,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.indigo.shade900,
|
||||||
|
letterSpacing: 1.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final w = MediaQuery.of(context).size.width;
|
final height = MediaQuery.of(context).size.height;
|
||||||
|
final width = MediaQuery.of(context).size.width;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
backgroundColor: const Color(0xFFE8F0FF),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: w * 0.06),
|
padding: EdgeInsets.symmetric(horizontal: width * 0.07),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 28),
|
/// Animated Welcome text
|
||||||
Align(alignment: Alignment.centerLeft, child: Text("Welcome", style: Theme.of(context).textTheme.headlineSmall)),
|
SlideTransition(
|
||||||
const SizedBox(height: 12),
|
position: _slide,
|
||||||
|
child: _shinyWelcomeText(),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: height * 0.01),
|
||||||
|
|
||||||
|
/// LOGO SECTION
|
||||||
|
Image.asset(
|
||||||
|
'assets/Images/K.png',
|
||||||
|
height: height * 0.28,
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: height * 0.015),
|
||||||
|
|
||||||
|
/// Description Text
|
||||||
const Text(
|
const Text(
|
||||||
"Register to access Kent Logistics services. After signup admin will review and approve your request. Approval may take up to 24 hours.",
|
"Register to access Kent Logistics services. After signup admin will review and approve your request. Approval may take up to 24 hours.",
|
||||||
style: TextStyle(fontSize: 15),
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
height: 1.4,
|
||||||
|
color: Colors.black87,
|
||||||
),
|
),
|
||||||
const Spacer(),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (_) => const SignupScreen())),
|
|
||||||
child: const SizedBox(width: double.infinity, child: Center(child: Padding(padding: EdgeInsets.all(14.0), child: Text("Create Account")))),
|
|
||||||
style: ElevatedButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
|
||||||
OutlinedButton(
|
SizedBox(height: height * 0.04),
|
||||||
onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (_) => const LoginScreen())),
|
|
||||||
child: const SizedBox(width: double.infinity, child: Center(child: Padding(padding: EdgeInsets.all(14.0), child: Text("Login")))),
|
/// 🌈 Create Account Button (Gradient)
|
||||||
style: OutlinedButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))),
|
PrimaryButton(
|
||||||
|
label: "Create Account",
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (_) => const SignupScreen()),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
SizedBox(height: height * 0.015),
|
||||||
|
|
||||||
|
/// 🌈 Login Button (Gradient)
|
||||||
|
PrimaryButton(
|
||||||
|
label: "Login",
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (_) => const LoginScreen()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: height * 0.02),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class AuthService {
|
|||||||
Future<Map<String, dynamic>> refreshToken(String oldToken) async {
|
Future<Map<String, dynamic>> refreshToken(String oldToken) async {
|
||||||
try {
|
try {
|
||||||
final response = await _dio.post(
|
final response = await _dio.post(
|
||||||
'/user/refresh',
|
'/auth/refresh',
|
||||||
options: Options(headers: {
|
options: Options(headers: {
|
||||||
'Authorization': 'Bearer $oldToken',
|
'Authorization': 'Bearer $oldToken',
|
||||||
}),
|
}),
|
||||||
|
|||||||
62
lib/services/chat_realtime_service.dart
Normal file
62
lib/services/chat_realtime_service.dart
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:pusher_channels_flutter/pusher_channels_flutter.dart';
|
||||||
|
import '../services/dio_client.dart';
|
||||||
|
|
||||||
|
class ChatRealtimeService {
|
||||||
|
static final PusherChannelsFlutter _pusher =
|
||||||
|
PusherChannelsFlutter.getInstance();
|
||||||
|
|
||||||
|
static Future<void> connect({
|
||||||
|
required int ticketId,
|
||||||
|
required Function(Map<String, dynamic>) onMessage,
|
||||||
|
}) async {
|
||||||
|
|
||||||
|
print(" 🧪🧪🧪🧪🧪🧪🧪🧪🧪SUBSCRIBING TO: private-ticket.$ticketId");
|
||||||
|
|
||||||
|
await _pusher.init(
|
||||||
|
apiKey: "q5fkk5rvcnatvbgadwvl",
|
||||||
|
cluster: "mt1",
|
||||||
|
|
||||||
|
onAuthorizer: (channelName, socketId, options) async {
|
||||||
|
print("🧪🧪🧪🧪🧪🧪🧪🧪🧪🧪AUTHORIZING CHANNEL: $channelName");
|
||||||
|
|
||||||
|
final dio = DioClient.getInstance(options);
|
||||||
|
|
||||||
|
final res = await dio.post(
|
||||||
|
'/broadcasting/auth',
|
||||||
|
data: {
|
||||||
|
'socket_id': socketId,
|
||||||
|
'channel_name': channelName,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
onEvent: (event) {
|
||||||
|
if (event.eventName == "NewChatMessage") {
|
||||||
|
final data = jsonDecode(event.data);
|
||||||
|
onMessage(Map<String, dynamic>.from(data));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onConnectionStateChange: (current, previous) {
|
||||||
|
print("PUSHER STATE: $current");
|
||||||
|
},
|
||||||
|
|
||||||
|
onError: (message, code, error) {
|
||||||
|
print("PUSHER ERROR: $message");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await _pusher.subscribe(
|
||||||
|
channelName: "private-ticket.$ticketId",
|
||||||
|
);
|
||||||
|
|
||||||
|
await _pusher.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> disconnect() async {
|
||||||
|
await _pusher.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
69
lib/services/chat_service.dart
Normal file
69
lib/services/chat_service.dart
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import '../config/api_config.dart';
|
||||||
|
|
||||||
|
class ChatService {
|
||||||
|
final Dio dio;
|
||||||
|
|
||||||
|
ChatService(this.dio);
|
||||||
|
|
||||||
|
/// Start chat / get ticket
|
||||||
|
Future<Map<String, dynamic>> startChat() async {
|
||||||
|
final res = await dio.get('/user/chat/start');
|
||||||
|
return Map<String, dynamic>.from(res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all messages
|
||||||
|
Future<List<dynamic>> getMessages(int ticketId) async {
|
||||||
|
final res = await dio.get('/user/chat/messages/$ticketId');
|
||||||
|
return res.data['messages'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send message (text or file)
|
||||||
|
Future<void> sendMessage(
|
||||||
|
int ticketId, {
|
||||||
|
String? message,
|
||||||
|
String? clientId,
|
||||||
|
}) async {
|
||||||
|
final form = FormData();
|
||||||
|
|
||||||
|
if (message != null) form.fields.add(MapEntry('message', message));
|
||||||
|
if (clientId != null) form.fields.add(MapEntry('client_id', clientId));
|
||||||
|
|
||||||
|
await dio.post('/user/chat/send/$ticketId', data: form);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// SEND FILE (image/video/pdf/excel)
|
||||||
|
// ---------------------------
|
||||||
|
Future<Map<String, dynamic>> sendFile(
|
||||||
|
int ticketId,
|
||||||
|
File file, {
|
||||||
|
required Function(double) onProgress,
|
||||||
|
}) async {
|
||||||
|
final formData = FormData.fromMap({
|
||||||
|
'file': await MultipartFile.fromFile(
|
||||||
|
file.path,
|
||||||
|
filename: file.path.split('/').last,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
final res = await dio.post(
|
||||||
|
"/user/chat/send/$ticketId",
|
||||||
|
data: formData,
|
||||||
|
options: Options(
|
||||||
|
headers: {'Content-Type': 'multipart/form-data'},
|
||||||
|
),
|
||||||
|
onSendProgress: (sent, total) {
|
||||||
|
if (total > 0) {
|
||||||
|
onProgress(sent / total);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return Map<String, dynamic>.from(res.data['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -6,7 +6,10 @@ import '../providers/auth_provider.dart';
|
|||||||
import 'token_interceptor.dart';
|
import 'token_interceptor.dart';
|
||||||
|
|
||||||
class DioClient {
|
class DioClient {
|
||||||
static Dio? _dio; // Singleton instance
|
static Dio? _dio;
|
||||||
|
|
||||||
|
// static const String baseUrl = "http://103.248.30.24:3030";
|
||||||
|
static const String baseUrl = "http://10.119.0.74:8000";
|
||||||
|
|
||||||
static Dio getInstance(BuildContext context) {
|
static Dio getInstance(BuildContext context) {
|
||||||
if (_dio == null) {
|
if (_dio == null) {
|
||||||
|
|||||||
@@ -29,4 +29,10 @@ class OrderService {
|
|||||||
final res = await _dio.get('/user/order/$id/track');
|
final res = await _dio.get('/user/order/$id/track');
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> confirmOrder(String orderId) async {
|
||||||
|
final res = await _dio.post('/user/orders/$orderId/confirm');
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
210
lib/services/reverb_socket_service.dart
Normal file
210
lib/services/reverb_socket_service.dart
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../services/dio_client.dart';
|
||||||
|
|
||||||
|
class ReverbSocketService {
|
||||||
|
WebSocketChannel? _channel;
|
||||||
|
Timer? _pingTimer;
|
||||||
|
bool _connected = false;
|
||||||
|
|
||||||
|
late BuildContext _context;
|
||||||
|
late int _ticketId;
|
||||||
|
late Function(Map<String, dynamic>) _onMessage;
|
||||||
|
late VoidCallback _onBackgroundMessage;
|
||||||
|
late VoidCallback _onAdminMessage;
|
||||||
|
|
||||||
|
|
||||||
|
/// Prevent duplicate messages on reconnect
|
||||||
|
final Set<int> _receivedIds = {};
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// CONNECT
|
||||||
|
// ============================
|
||||||
|
Future<void> connect({
|
||||||
|
required BuildContext context,
|
||||||
|
required int ticketId,
|
||||||
|
required Function(Map<String, dynamic>) onMessage,
|
||||||
|
//required VoidCallback onBackgroundMessage,
|
||||||
|
required VoidCallback onAdminMessage, // 👈 ADD
|
||||||
|
}) async {
|
||||||
|
_context = context;
|
||||||
|
_ticketId = ticketId;
|
||||||
|
_onMessage = onMessage;
|
||||||
|
//_onBackgroundMessage = onBackgroundMessage;
|
||||||
|
_onAdminMessage = onAdminMessage; // 👈 SAVE
|
||||||
|
|
||||||
|
final uri = Uri.parse(
|
||||||
|
'ws://10.119.0.74:8080/app/q5fkk5rvcnatvbgadwvl'
|
||||||
|
'?protocol=7&client=flutter&version=1.0',
|
||||||
|
);
|
||||||
|
// final uri = Uri.parse(
|
||||||
|
// 'ws://103.248.30.24:8080/app/q5fkk5rvcnatvbgadwvl'
|
||||||
|
// '?protocol=7&client=flutter&version=1.0',
|
||||||
|
// );
|
||||||
|
|
||||||
|
debugPrint("🔌 CONNECTING SOCKET → $uri");
|
||||||
|
|
||||||
|
_channel = WebSocketChannel.connect(uri);
|
||||||
|
|
||||||
|
_channel!.stream.listen(
|
||||||
|
_handleMessage,
|
||||||
|
onDone: _handleDisconnect,
|
||||||
|
onError: (e) {
|
||||||
|
debugPrint("❌ SOCKET ERROR: $e");
|
||||||
|
_handleDisconnect();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// HANDLE SOCKET MESSAGE
|
||||||
|
// ============================
|
||||||
|
Future<void> _handleMessage(dynamic raw) async {
|
||||||
|
debugPrint("📥 RAW: $raw");
|
||||||
|
|
||||||
|
|
||||||
|
final payload = jsonDecode(raw);
|
||||||
|
final event = payload['event']?.toString() ?? '';
|
||||||
|
|
||||||
|
// --------------------------------
|
||||||
|
// CONNECTED
|
||||||
|
// --------------------------------
|
||||||
|
if (event == 'pusher:connection_established') {
|
||||||
|
final socketId = jsonDecode(payload['data'])['socket_id'];
|
||||||
|
debugPrint("✅ CONNECTED | socket_id=$socketId");
|
||||||
|
|
||||||
|
await _subscribe(socketId);
|
||||||
|
_startHeartbeat();
|
||||||
|
_connected = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------
|
||||||
|
// HEARTBEAT
|
||||||
|
// --------------------------------
|
||||||
|
if (event == 'pusher:pong') {
|
||||||
|
debugPrint("💓 PONG");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------
|
||||||
|
// CHAT MESSAGE
|
||||||
|
// --------------------------------
|
||||||
|
if (
|
||||||
|
event == 'NewChatMessage' ||
|
||||||
|
event.endsWith('.NewChatMessage') ||
|
||||||
|
event.contains('NewChatMessage')) {
|
||||||
|
|
||||||
|
dynamic data = payload['data'];
|
||||||
|
if (data is String) data = jsonDecode(data);
|
||||||
|
|
||||||
|
final int msgId = data['id'];
|
||||||
|
final String senderType = data['sender_type'] ?? '';
|
||||||
|
final String? incomingClientId = data['client_id']; // ✅ HERE
|
||||||
|
|
||||||
|
// 🔁 Prevent duplicates by DB id
|
||||||
|
if (_receivedIds.contains(msgId)) {
|
||||||
|
debugPrint("🔁 DUPLICATE MESSAGE IGNORED: $msgId");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_receivedIds.add(msgId);
|
||||||
|
|
||||||
|
debugPrint("📩 NEW MESSAGE");
|
||||||
|
debugPrint("🆔 id=$msgId");
|
||||||
|
debugPrint("🧩 client_id=$incomingClientId");
|
||||||
|
debugPrint("👤 sender=$senderType");
|
||||||
|
debugPrint("💬 text=${data['message']}");
|
||||||
|
|
||||||
|
// ✅ Forward FULL payload (with client_id) to UI
|
||||||
|
_onMessage(Map<String, dynamic>.from(data));
|
||||||
|
|
||||||
|
// 🔔 Unread count only for admin messages
|
||||||
|
if (senderType == 'App\\Models\\Admin') {
|
||||||
|
debugPrint("🔔 ADMIN MESSAGE → UNREAD +1");
|
||||||
|
_onAdminMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// SUBSCRIBE PRIVATE CHANNEL
|
||||||
|
// ============================
|
||||||
|
Future<void> _subscribe(String socketId) async {
|
||||||
|
debugPrint("📡 SUBSCRIBING private-ticket.$_ticketId");
|
||||||
|
|
||||||
|
final dio = DioClient.getInstance(_context);
|
||||||
|
|
||||||
|
final res = await dio.post(
|
||||||
|
'/broadcasting/auth',
|
||||||
|
data: {
|
||||||
|
'socket_id': socketId,
|
||||||
|
'channel_name': 'private-ticket.$_ticketId',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
_channel!.sink.add(jsonEncode({
|
||||||
|
'event': 'pusher:subscribe',
|
||||||
|
'data': {
|
||||||
|
'channel': 'private-ticket.$_ticketId',
|
||||||
|
'auth': res.data['auth'],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
debugPrint("✅ SUBSCRIBED private-ticket.$_ticketId");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// HEARTBEAT
|
||||||
|
// ============================
|
||||||
|
void _startHeartbeat() {
|
||||||
|
_pingTimer?.cancel();
|
||||||
|
|
||||||
|
_pingTimer = Timer.periodic(
|
||||||
|
const Duration(seconds: 30),
|
||||||
|
(_) {
|
||||||
|
if (_connected) {
|
||||||
|
debugPrint("💓 SENDING PING");
|
||||||
|
_channel?.sink.add(jsonEncode({
|
||||||
|
'event': 'pusher:ping',
|
||||||
|
'data': {}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// HANDLE DISCONNECT
|
||||||
|
// ============================
|
||||||
|
void _handleDisconnect() {
|
||||||
|
debugPrint("⚠️ SOCKET DISCONNECTED → RECONNECTING");
|
||||||
|
|
||||||
|
_connected = false;
|
||||||
|
_pingTimer?.cancel();
|
||||||
|
_channel?.sink.close();
|
||||||
|
|
||||||
|
Future.delayed(const Duration(seconds: 2), () {
|
||||||
|
connect(
|
||||||
|
context: _context,
|
||||||
|
ticketId: _ticketId,
|
||||||
|
onMessage: _onMessage,
|
||||||
|
// onBackgroundMessage: _onBackgroundMessage,
|
||||||
|
onAdminMessage: _onAdminMessage, // 👈 ADD
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// MANUAL DISCONNECT
|
||||||
|
// ============================
|
||||||
|
void disconnect() {
|
||||||
|
debugPrint("❌ SOCKET CLOSED MANUALLY");
|
||||||
|
_connected = false;
|
||||||
|
_pingTimer?.cancel();
|
||||||
|
_channel?.sink.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,37 +1,130 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import '../config/api_config.dart';
|
||||||
import '../providers/auth_provider.dart';
|
import '../providers/auth_provider.dart';
|
||||||
|
|
||||||
class TokenInterceptor extends Interceptor {
|
class TokenInterceptor extends Interceptor {
|
||||||
final AuthProvider auth;
|
final AuthProvider authProvider;
|
||||||
final BuildContext context;
|
final BuildContext context;
|
||||||
final Dio dio;
|
final Dio dio;
|
||||||
|
|
||||||
TokenInterceptor(this.auth, this.context, this.dio);
|
Completer<bool>? _refreshCompleter;
|
||||||
|
|
||||||
|
TokenInterceptor(this.authProvider, this.context, this.dio);
|
||||||
|
|
||||||
|
// 🔐 Attach token to every request
|
||||||
@override
|
@override
|
||||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
void onRequest(
|
||||||
if (auth.token != null) {
|
RequestOptions options, RequestInterceptorHandler handler) async {
|
||||||
options.headers['Authorization'] = 'Bearer ${auth.token}';
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final token = prefs.getString('token');
|
||||||
|
|
||||||
|
if (token != null && token.isNotEmpty) {
|
||||||
|
options.headers['Authorization'] = 'Bearer $token';
|
||||||
|
debugPrint('🔐🔐🔐🔐🔐🔐 [REQUEST] Token attached → ${options.uri}');
|
||||||
|
} else {
|
||||||
|
debugPrint('⚠️⚠️⚠️⚠️⚠️⚠️ [REQUEST] No token found → ${options.uri}');
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.next(options);
|
handler.next(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔄 Handle 401 & refresh token
|
||||||
@override
|
@override
|
||||||
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
||||||
if (err.response?.statusCode == 401 &&
|
debugPrint(
|
||||||
err.response?.data['message'] == 'Token has expired') {
|
'❌❌❌❌❌ [ERROR] ${err.response?.statusCode} on ${err.requestOptions.uri}');
|
||||||
|
|
||||||
final refreshed = await auth.tryRefreshToken(context);
|
if (err.response?.statusCode == 401) {
|
||||||
|
debugPrint('🔄🔄🔄🔄🔄🔄🔄 [AUTH] 401 detected, attempting refresh…');
|
||||||
|
|
||||||
|
// If refresh already running, wait
|
||||||
|
if (_refreshCompleter != null) {
|
||||||
|
debugPrint('⏳⏳⏳⏳⏳⏳⏳⏳⏳ [REFRESH] Waiting for ongoing refresh to complete…');
|
||||||
|
|
||||||
|
final success = await _refreshCompleter!.future;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
debugPrint('✅✅✅✅✅✅✅✅ [REFRESH] Token refreshed, retrying request');
|
||||||
|
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
err.requestOptions.headers['Authorization'] =
|
||||||
|
'Bearer ${prefs.getString('token')}';
|
||||||
|
|
||||||
|
final response = await dio.fetch(err.requestOptions);
|
||||||
|
return handler.resolve(response);
|
||||||
|
} else {
|
||||||
|
debugPrint('❌❌❌❌❌❌ [REFRESH] Refresh failed while waiting');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_refreshCompleter = Completer<bool>();
|
||||||
|
|
||||||
|
debugPrint('🚀🚀🚀🚀🚀🚀🚀🚀 [REFRESH] Starting new refresh request');
|
||||||
|
|
||||||
|
final refreshed = await _refreshToken();
|
||||||
|
|
||||||
|
_refreshCompleter!.complete(refreshed);
|
||||||
|
_refreshCompleter = null;
|
||||||
|
|
||||||
if (refreshed) {
|
if (refreshed) {
|
||||||
err.requestOptions.headers['Authorization'] = 'Bearer ${auth.token}';
|
debugPrint('✅✅✅✅✅✅✅✅✅ [REFRESH] Refresh successful, retrying original request');
|
||||||
final newResponse = await dio.fetch(err.requestOptions);
|
|
||||||
return handler.resolve(newResponse);
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
err.requestOptions.headers['Authorization'] =
|
||||||
|
'Bearer ${prefs.getString('token')}';
|
||||||
|
|
||||||
|
final response = await dio.fetch(err.requestOptions);
|
||||||
|
return handler.resolve(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debugPrint('🚪🚪🚪🚪🚪🚪 [AUTH] Refresh failed → logging out user');
|
||||||
|
await authProvider.logout(context);
|
||||||
|
//await authProvider.forceLogout(context);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.next(err);
|
handler.next(err);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
// 🔁 Call refresh API using SEPARATE Dio
|
||||||
|
Future<bool> _refreshToken() async {
|
||||||
|
try {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final oldToken = prefs.getString('token');
|
||||||
|
|
||||||
|
if (oldToken == null) {
|
||||||
|
debugPrint('❌❌❌❌❌❌❌ [REFRESH] No old token found');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('📤📤📤📤📤 [REFRESH] Calling /auth/refresh');
|
||||||
|
|
||||||
|
final refreshDio = Dio(
|
||||||
|
BaseOptions(
|
||||||
|
baseUrl: ApiConfig.baseUrl,
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer $oldToken',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final res = await refreshDio.post('/auth/refresh');
|
||||||
|
|
||||||
|
if (res.data['success'] == true && res.data['token'] != null) {
|
||||||
|
await prefs.setString('token', res.data['token']);
|
||||||
|
debugPrint('💾💾💾💾💾💾💾 [REFRESH] New token saved to SharedPreferences');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('❌❌❌❌❌❌ [REFRESH] API responded but refresh not allowed');
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('🔥🔥🔥🔥🔥🔥 [REFRESH] Exception occurred: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
118
lib/widgets/chat_file_actions.dart
Normal file
118
lib/widgets/chat_file_actions.dart
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import '../screens/chat_file_viewer.dart';
|
||||||
|
|
||||||
|
class ChatFileActions {
|
||||||
|
static void show(
|
||||||
|
BuildContext context, {
|
||||||
|
required String url,
|
||||||
|
required String fileType,
|
||||||
|
}) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
|
),
|
||||||
|
builder: (_) {
|
||||||
|
return SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
_actionTile(
|
||||||
|
icon: Icons.open_in_new,
|
||||||
|
title: "Open",
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
ChatFileViewer.open(
|
||||||
|
context,
|
||||||
|
url: url,
|
||||||
|
fileType: fileType,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_actionTile(
|
||||||
|
icon: Icons.download,
|
||||||
|
title: "Download",
|
||||||
|
onTap: () async {
|
||||||
|
Navigator.pop(context);
|
||||||
|
await _downloadFile(context, url, fileType);
|
||||||
|
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Widget _actionTile({
|
||||||
|
required IconData icon,
|
||||||
|
required String title,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(icon),
|
||||||
|
title: Text(title),
|
||||||
|
onTap: onTap,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static Future<void> _scanFile(String path) async {
|
||||||
|
const platform = MethodChannel('media_scanner');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await platform.invokeMethod('scanFile', {'path': path});
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ===========================
|
||||||
|
// DOWNLOAD LOGIC
|
||||||
|
// ===========================
|
||||||
|
static Future<void> _downloadFile(
|
||||||
|
BuildContext context,
|
||||||
|
String url,
|
||||||
|
String fileType,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final fileName = url.split('/').last;
|
||||||
|
|
||||||
|
Directory baseDir;
|
||||||
|
|
||||||
|
if (fileType.startsWith('image/')) {
|
||||||
|
baseDir = Directory('/storage/emulated/0/Pictures/KentChat');
|
||||||
|
} else if (fileType.startsWith('video/')) {
|
||||||
|
baseDir = Directory('/storage/emulated/0/Movies/KentChat');
|
||||||
|
} else {
|
||||||
|
baseDir = Directory('/storage/emulated/0/Download/KentChat');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await baseDir.exists()) {
|
||||||
|
await baseDir.create(recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
final filePath = "${baseDir.path}/$fileName";
|
||||||
|
|
||||||
|
await Dio().download(url, filePath);
|
||||||
|
|
||||||
|
// 🔔 IMPORTANT: Tell Android to scan the file
|
||||||
|
await _scanFile(filePath);
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Saved to ${baseDir.path}")),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text("Download failed")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
127
lib/widgets/chat_file_preview.dart
Normal file
127
lib/widgets/chat_file_preview.dart
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../services/dio_client.dart';
|
||||||
|
import '../screens/chat_file_viewer.dart';
|
||||||
|
import '../widgets/chat_file_actions.dart';
|
||||||
|
|
||||||
|
class ChatFilePreview extends StatelessWidget {
|
||||||
|
final String filePath;
|
||||||
|
final String fileType;
|
||||||
|
final bool isUser;
|
||||||
|
|
||||||
|
final bool isLocal;
|
||||||
|
|
||||||
|
const ChatFilePreview({
|
||||||
|
super.key,
|
||||||
|
required this.filePath,
|
||||||
|
required this.fileType,
|
||||||
|
required this.isUser,
|
||||||
|
this.isLocal = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final url = "${DioClient.baseUrl}/storage/$filePath";
|
||||||
|
final textColor = isUser ? Colors.white : Colors.black;
|
||||||
|
|
||||||
|
// LOCAL IMAGE (uploading preview)
|
||||||
|
if (isLocal && fileType.startsWith('image/')) {
|
||||||
|
return Image.file(
|
||||||
|
File(filePath),
|
||||||
|
width: 180,
|
||||||
|
height: 120,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
// ✅ TAP = OPEN
|
||||||
|
ChatFileViewer.open(
|
||||||
|
context,
|
||||||
|
url: url,
|
||||||
|
fileType: fileType,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onLongPress: () {
|
||||||
|
// ✅ LONG PRESS = OPEN / DOWNLOAD
|
||||||
|
ChatFileActions.show(
|
||||||
|
context,
|
||||||
|
url: url,
|
||||||
|
fileType: fileType,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: _buildPreviewUI(textColor, url),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPreviewUI(Color textColor, String url) {
|
||||||
|
// IMAGE
|
||||||
|
if (fileType.startsWith('image/')) {
|
||||||
|
return ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Image.network(
|
||||||
|
url,
|
||||||
|
width: 180,
|
||||||
|
height: 120,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// VIDEO
|
||||||
|
if (fileType.startsWith('video/')) {
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 180,
|
||||||
|
height: 120,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black26,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.videocam,
|
||||||
|
size: 50,
|
||||||
|
color: Colors.white70,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Icon(
|
||||||
|
Icons.play_circle_fill,
|
||||||
|
size: 56,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDF / OTHER FILES
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(_fileIcon(fileType), color: textColor),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
_fileLabel(fileType),
|
||||||
|
style: TextStyle(color: textColor),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
IconData _fileIcon(String type) {
|
||||||
|
if (type == 'application/pdf') return Icons.picture_as_pdf;
|
||||||
|
if (type.contains('excel')) return Icons.table_chart;
|
||||||
|
return Icons.insert_drive_file;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _fileLabel(String type) {
|
||||||
|
if (type == 'application/pdf') return "PDF document";
|
||||||
|
if (type.contains('excel')) return "Excel file";
|
||||||
|
return "Download file";
|
||||||
|
}
|
||||||
|
}
|
||||||
356
lib/widgets/invoice_detail_view.dart
Normal file
356
lib/widgets/invoice_detail_view.dart
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class InvoiceDetailView extends StatefulWidget {
|
||||||
|
final Map invoice;
|
||||||
|
const InvoiceDetailView({super.key, required this.invoice});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<InvoiceDetailView> createState() => _InvoiceDetailViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _InvoiceDetailViewState extends State<InvoiceDetailView>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<Offset> _slideAnimation;
|
||||||
|
|
||||||
|
bool s1 = true;
|
||||||
|
bool s2 = false;
|
||||||
|
bool s3 = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 350),
|
||||||
|
);
|
||||||
|
|
||||||
|
_slideAnimation = Tween(
|
||||||
|
begin: const Offset(0, -0.1),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(
|
||||||
|
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
|
||||||
|
);
|
||||||
|
|
||||||
|
_controller.forward();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// STATUS BADGE (WHITE + GRADIENT BORDER)
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
Widget statusBadge(String status, double scale) {
|
||||||
|
final s = status.toLowerCase();
|
||||||
|
|
||||||
|
LinearGradient gradient;
|
||||||
|
Color textColor;
|
||||||
|
|
||||||
|
switch (s) {
|
||||||
|
case 'paid':
|
||||||
|
gradient = const LinearGradient(
|
||||||
|
colors: [Color(0xFF2ECC71), Color(0xFF27AE60)],
|
||||||
|
);
|
||||||
|
textColor = const Color(0xFF27AE60);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'pending':
|
||||||
|
gradient = const LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Color(0xFF5B8DEF), // Soft Blue
|
||||||
|
Color(0xFF7B5CFA), // Purple Blue
|
||||||
|
],
|
||||||
|
);
|
||||||
|
textColor = const Color(0xFF5B8DEF);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'overdue':
|
||||||
|
gradient = const LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Color(0xFFFFB300), // Amber
|
||||||
|
Color(0xFFFF6F00), // Deep Orange
|
||||||
|
],
|
||||||
|
);
|
||||||
|
textColor = const Color(0xFFFF6F00);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'cancelled':
|
||||||
|
case 'failed':
|
||||||
|
gradient = const LinearGradient(
|
||||||
|
colors: [Color(0xFFE74C3C), Color(0xFFC0392B)],
|
||||||
|
);
|
||||||
|
textColor = const Color(0xFFE74C3C);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
gradient = const LinearGradient(
|
||||||
|
colors: [Color(0xFF95A5A6), Color(0xFF7F8C8D)],
|
||||||
|
);
|
||||||
|
textColor = Colors.grey.shade700;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(1.5),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: gradient,
|
||||||
|
borderRadius: BorderRadius.circular(20 * scale),
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 14 * scale,
|
||||||
|
vertical: 6 * scale,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(18 * scale),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
status.toUpperCase(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12 * scale,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: textColor,
|
||||||
|
letterSpacing: .6,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// HEADER CARD
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
Widget headerCard(Map invoice, double scale) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: EdgeInsets.all(16 * scale),
|
||||||
|
margin: EdgeInsets.only(bottom: 14 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [Colors.blue.shade400, Colors.indigo.shade600],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(16 * scale),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
blurRadius: 8 * scale,
|
||||||
|
offset: Offset(0, 3 * scale),
|
||||||
|
color: Colors.indigo.withOpacity(.3),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Invoice #${invoice['invoice_number']}",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20 * scale,
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 4 * scale),
|
||||||
|
Text(
|
||||||
|
"Date: ${invoice['invoice_date']}",
|
||||||
|
style: TextStyle(color: Colors.white70, fontSize: 13 * scale),
|
||||||
|
),
|
||||||
|
SizedBox(height: 10 * scale),
|
||||||
|
|
||||||
|
/// STATUS BADGE
|
||||||
|
statusBadge(invoice['status'] ?? 'Unknown', scale),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// SECTION HEADER
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
Widget sectionHeader(
|
||||||
|
String title,
|
||||||
|
IconData icon,
|
||||||
|
bool expanded,
|
||||||
|
VoidCallback tap,
|
||||||
|
double scale,
|
||||||
|
) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: tap,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.all(12 * scale),
|
||||||
|
margin: EdgeInsets.only(bottom: 8 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [Colors.indigo.shade500, Colors.blue.shade400],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(12 * scale),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
blurRadius: 5 * scale,
|
||||||
|
color: Colors.black.withOpacity(.15),
|
||||||
|
offset: Offset(0, 2 * scale),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: Colors.white, size: 20 * scale),
|
||||||
|
SizedBox(width: 8 * scale),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15 * scale,
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
AnimatedRotation(
|
||||||
|
turns: expanded ? .5 : 0,
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
child: Icon(
|
||||||
|
Icons.keyboard_arrow_down,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 20 * scale,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// SECTION BODY
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
Widget sectionBody(bool visible, List<Widget> children, double scale) {
|
||||||
|
return AnimatedCrossFade(
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
crossFadeState:
|
||||||
|
visible ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
||||||
|
firstChild: const SizedBox.shrink(),
|
||||||
|
secondChild: SlideTransition(
|
||||||
|
position: _slideAnimation,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.all(14 * scale),
|
||||||
|
margin: EdgeInsets.only(bottom: 10 * scale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).cardColor,
|
||||||
|
borderRadius: BorderRadius.circular(12 * scale),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
blurRadius: 7 * scale,
|
||||||
|
offset: Offset(0, 2 * scale),
|
||||||
|
color: Colors.black.withOpacity(.07),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(children: children),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// DETAIL ROW
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
Widget detailRow(IconData icon, String label, dynamic value, double scale) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 5 * scale),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 18 * scale, color: Colors.blueGrey),
|
||||||
|
SizedBox(width: 8 * scale),
|
||||||
|
Expanded(
|
||||||
|
flex: 3,
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
fontSize: 13 * scale,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 4,
|
||||||
|
child: label == "Status"
|
||||||
|
? Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: statusBadge(value ?? 'Unknown', scale),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
value?.toString() ?? "N/A",
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14 * scale,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// MAIN BUILD
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final invoice = widget.invoice;
|
||||||
|
final width = MediaQuery.of(context).size.width;
|
||||||
|
final scale = (width / 390).clamp(0.85, 1.25);
|
||||||
|
|
||||||
|
return ListView(
|
||||||
|
padding: EdgeInsets.all(14 * scale),
|
||||||
|
children: [
|
||||||
|
headerCard(invoice, scale),
|
||||||
|
|
||||||
|
sectionHeader(
|
||||||
|
"Invoice Summary",
|
||||||
|
Icons.receipt_long,
|
||||||
|
s1,
|
||||||
|
() => setState(() => s1 = !s1),
|
||||||
|
scale,
|
||||||
|
),
|
||||||
|
sectionBody(s1, [
|
||||||
|
detailRow(Icons.numbers, "Invoice No", invoice['invoice_number'], scale),
|
||||||
|
detailRow(Icons.calendar_today, "Date", invoice['invoice_date'], scale),
|
||||||
|
detailRow(Icons.label, "Status", invoice['status'], scale),
|
||||||
|
], scale),
|
||||||
|
|
||||||
|
sectionHeader(
|
||||||
|
"Customer Details",
|
||||||
|
Icons.person,
|
||||||
|
s2,
|
||||||
|
() => setState(() => s2 = !s2),
|
||||||
|
scale,
|
||||||
|
),
|
||||||
|
sectionBody(s2, [
|
||||||
|
detailRow(Icons.person_outline, "Name", invoice['customer_name'], scale),
|
||||||
|
detailRow(Icons.mail, "Email", invoice['customer_email'], scale),
|
||||||
|
detailRow(Icons.phone, "Phone", invoice['customer_mobile'], scale),
|
||||||
|
detailRow(Icons.location_on, "Address", invoice['customer_address'], scale),
|
||||||
|
], scale),
|
||||||
|
|
||||||
|
sectionHeader(
|
||||||
|
"Amount Details",
|
||||||
|
Icons.currency_rupee,
|
||||||
|
s3,
|
||||||
|
() => setState(() => s3 = !s3),
|
||||||
|
scale,
|
||||||
|
),
|
||||||
|
sectionBody(s3, [
|
||||||
|
detailRow(Icons.money, "Amount", invoice['final_amount'], scale),
|
||||||
|
detailRow(Icons.percent, "GST %", invoice['gst_percent'], scale),
|
||||||
|
detailRow(Icons.percent, "GST Amount", invoice['gst_amount'], scale),
|
||||||
|
detailRow(Icons.summarize, "Total", invoice['final_amount_with_gst'], scale),
|
||||||
|
], scale),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ class MainAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
final profileUrl = profileProvider.profile?.profileImage;
|
final profileUrl = profileProvider.profile?.profileImage;
|
||||||
|
|
||||||
return AppBar(
|
return AppBar(
|
||||||
backgroundColor: Colors.lightGreen,
|
backgroundColor: Colors.white,
|
||||||
elevation: 0.8,
|
elevation: 0.8,
|
||||||
surfaceTintColor: Colors.transparent,
|
surfaceTintColor: Colors.transparent,
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
|
|||||||
@@ -1,19 +1,62 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class PrimaryButton extends StatelessWidget {
|
class PrimaryButton extends StatelessWidget {
|
||||||
final String label;
|
final String label;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
final bool busy;
|
final bool busy;
|
||||||
const PrimaryButton({super.key, required this.label, required this.onTap, this.busy = false});
|
|
||||||
|
const PrimaryButton({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
required this.onTap,
|
||||||
|
this.busy = false,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
|
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Color(0xFF0D47A1), // Blue
|
||||||
|
Color(0xFF6A1B9A), // Purple
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: busy ? null : onTap,
|
onPressed: busy ? null : onTap,
|
||||||
style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))),
|
style: ElevatedButton.styleFrom(
|
||||||
child: busy ? const SizedBox(height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) : Text(label, style: const TextStyle(fontSize: 16)),
|
backgroundColor: Colors
|
||||||
|
.transparent, // IMPORTANT: keep transparent to see gradient
|
||||||
|
shadowColor: Colors.transparent,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
child: busy
|
||||||
|
? const SizedBox(
|
||||||
|
height: 18,
|
||||||
|
width: 18,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(fontSize: 16, color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,16 +19,27 @@ class RoundedInput extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final radius = BorderRadius.circular(12);
|
final radius = BorderRadius.circular(12);
|
||||||
|
|
||||||
return TextField(
|
return TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
keyboardType: keyboardType,
|
keyboardType: keyboardType,
|
||||||
obscureText: obscure,
|
obscureText: obscure,
|
||||||
maxLines: maxLines,
|
maxLines: obscure ? 1 : maxLines,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.grey, // grey input text
|
||||||
|
),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
filled: true,
|
filled: true,
|
||||||
|
fillColor: const Color(0xFFD8E7FF), // light blue background
|
||||||
hintText: hint,
|
hintText: hint,
|
||||||
|
hintStyle: const TextStyle(
|
||||||
|
color: Colors.grey, // grey hint text
|
||||||
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
border: OutlineInputBorder(borderRadius: radius, borderSide: BorderSide.none),
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: radius,
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,13 @@
|
|||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <file_selector_linux/file_selector_plugin.h>
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||||
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
file_selector_linux
|
file_selector_linux
|
||||||
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
|||||||
@@ -6,11 +6,21 @@ import FlutterMacOS
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import file_selector_macos
|
import file_selector_macos
|
||||||
|
import package_info_plus
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
|
import share_plus
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
|
import url_launcher_macos
|
||||||
|
import video_player_avfoundation
|
||||||
|
import wakelock_plus
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
|
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
|
||||||
|
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
418
pubspec.lock
418
pubspec.lock
@@ -1,6 +1,22 @@
|
|||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
|
archive:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.7"
|
||||||
|
args:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: args
|
||||||
|
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -9,6 +25,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.13.0"
|
version: "2.13.0"
|
||||||
|
barcode:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: barcode
|
||||||
|
sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.9"
|
||||||
|
bidi:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: bidi
|
||||||
|
sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.13"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -25,6 +57,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
chewie:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: chewie
|
||||||
|
sha256: "44bcfc5f0dfd1de290c87c9d86a61308b3282a70b63435d5557cfd60f54a69ca"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.13.0"
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -57,6 +97,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.7"
|
version: "3.0.7"
|
||||||
|
csslib:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: csslib
|
||||||
|
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -65,6 +113,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.8"
|
version: "1.0.8"
|
||||||
|
dbus:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dbus
|
||||||
|
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.11"
|
||||||
dio:
|
dio:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -105,6 +161,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.1"
|
version: "7.0.1"
|
||||||
|
file_selector:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: file_selector
|
||||||
|
sha256: "5f1d15a7f17115038f433d1b0ea57513cc9e29a9d5338d166cb0bef3fa90a7a0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
|
file_selector_android:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: file_selector_android
|
||||||
|
sha256: "1ce58b609289551f8ec07265476720e77d19764339cc1d8e4df3c4d34dac6499"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.1+17"
|
||||||
|
file_selector_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_ios
|
||||||
|
sha256: fe9f52123af16bba4ad65bd7e03defbbb4b172a38a8e6aaa2a869a0c56a5f5fb
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.3+2"
|
||||||
file_selector_linux:
|
file_selector_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -129,6 +209,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.7.0"
|
version: "2.7.0"
|
||||||
|
file_selector_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_web
|
||||||
|
sha256: c4c0ea4224d97a60a7067eca0c8fd419e708ff830e0c83b11a48faf566cec3e7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.4+2"
|
||||||
file_selector_windows:
|
file_selector_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -137,6 +225,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.3+5"
|
version: "0.9.3+5"
|
||||||
|
fixnum:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fixnum
|
||||||
|
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -150,6 +246,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0"
|
version: "5.0.0"
|
||||||
|
flutter_pdfview:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_pdfview
|
||||||
|
sha256: c0b2cc4ebf461a5a4bb9312a165222475a7d93845c7a0703f4abb7f442eb6d54
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.3"
|
||||||
flutter_plugin_android_lifecycle:
|
flutter_plugin_android_lifecycle:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -176,6 +280,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.5"
|
version: "4.0.5"
|
||||||
|
html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: html
|
||||||
|
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.6"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -192,6 +304,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
version: "4.1.2"
|
||||||
|
image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image
|
||||||
|
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.5.4"
|
||||||
image_picker:
|
image_picker:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -256,6 +376,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.2"
|
version: "0.2.2"
|
||||||
|
js:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: js
|
||||||
|
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.2"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -328,6 +456,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
|
package_info_plus:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_info_plus
|
||||||
|
sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.0.0"
|
||||||
|
package_info_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_info_plus_platform_interface
|
||||||
|
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.1"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -336,8 +480,16 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
version: "1.9.1"
|
||||||
path_provider:
|
path_parsing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_parsing
|
||||||
|
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
path_provider:
|
||||||
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path_provider
|
name: path_provider
|
||||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||||
@@ -384,6 +536,78 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
|
pdf:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: pdf
|
||||||
|
sha256: "28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.11.3"
|
||||||
|
permission_handler:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: permission_handler
|
||||||
|
sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.4.0"
|
||||||
|
permission_handler_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_android
|
||||||
|
sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "12.1.0"
|
||||||
|
permission_handler_apple:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_apple
|
||||||
|
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.4.7"
|
||||||
|
permission_handler_html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_html
|
||||||
|
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.3+5"
|
||||||
|
permission_handler_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_platform_interface
|
||||||
|
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.3.0"
|
||||||
|
permission_handler_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_windows
|
||||||
|
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.1"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.1"
|
||||||
|
photo_view:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: photo_view
|
||||||
|
sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.0"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -400,6 +624,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
|
posix:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: posix
|
||||||
|
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.3"
|
||||||
provider:
|
provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -408,6 +640,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.5+1"
|
version: "6.1.5+1"
|
||||||
|
pusher_channels_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: pusher_channels_flutter
|
||||||
|
sha256: "4d83b2012079c7d1a3c42cee1d37a48c974504d869b6a9c085144b2d4e35e58a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.0"
|
||||||
|
qr:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: qr
|
||||||
|
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.2"
|
||||||
|
share_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: share_plus
|
||||||
|
sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "10.1.4"
|
||||||
|
share_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: share_plus_platform_interface
|
||||||
|
sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.2"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -525,6 +789,78 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
url_launcher:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: url_launcher
|
||||||
|
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.2"
|
||||||
|
url_launcher_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_android
|
||||||
|
sha256: "81777b08c498a292d93ff2feead633174c386291e35612f8da438d6e92c4447e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.20"
|
||||||
|
url_launcher_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_ios
|
||||||
|
sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.4"
|
||||||
|
url_launcher_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_linux
|
||||||
|
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.2"
|
||||||
|
url_launcher_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_macos
|
||||||
|
sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.3"
|
||||||
|
url_launcher_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_platform_interface
|
||||||
|
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
|
url_launcher_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_web
|
||||||
|
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
url_launcher_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_windows
|
||||||
|
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.5"
|
||||||
|
uuid:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: uuid
|
||||||
|
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.5.2"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -533,6 +869,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.4"
|
||||||
|
video_player:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: video_player
|
||||||
|
sha256: "096bc28ce10d131be80dfb00c223024eb0fba301315a406728ab43dd99c45bdf"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.10.1"
|
||||||
|
video_player_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_android
|
||||||
|
sha256: a8dc4324f67705de057678372bedb66cd08572fe7c495605ac68c5f503324a39
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.8.15"
|
||||||
|
video_player_avfoundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_avfoundation
|
||||||
|
sha256: f9a780aac57802b2892f93787e5ea53b5f43cc57dc107bee9436458365be71cd
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.8.4"
|
||||||
|
video_player_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_platform_interface
|
||||||
|
sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.6.0"
|
||||||
|
video_player_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_web
|
||||||
|
sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -541,6 +917,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "15.0.0"
|
version: "15.0.0"
|
||||||
|
wakelock_plus:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: wakelock_plus
|
||||||
|
sha256: "9296d40c9adbedaba95d1e704f4e0b434be446e2792948d0e4aa977048104228"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
|
wakelock_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: wakelock_plus_platform_interface
|
||||||
|
sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
web:
|
web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -549,6 +941,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
|
web_socket_channel:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: web_socket_channel
|
||||||
|
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
|
win32:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32
|
||||||
|
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.15.0"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -557,6 +965,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
|
xml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xml
|
||||||
|
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.6.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.8.1 <4.0.0"
|
dart: ">=3.8.1 <4.0.0"
|
||||||
flutter: ">=3.32.0"
|
flutter: ">=3.32.0"
|
||||||
|
|||||||
16
pubspec.yaml
16
pubspec.yaml
@@ -37,6 +37,20 @@ dependencies:
|
|||||||
google_fonts: ^4.0.3
|
google_fonts: ^4.0.3
|
||||||
image_picker: ^1.0.7
|
image_picker: ^1.0.7
|
||||||
|
|
||||||
|
share_plus: ^10.0.0
|
||||||
|
path_provider: ^2.1.2
|
||||||
|
pdf: ^3.11.0
|
||||||
|
pusher_channels_flutter: ^2.3.0
|
||||||
|
web_socket_channel: ^2.4.0
|
||||||
|
url_launcher: ^6.2.6
|
||||||
|
file_selector: ^1.0.3
|
||||||
|
file_selector_android: ^0.5.0
|
||||||
|
photo_view: ^0.15.0
|
||||||
|
video_player: ^2.9.1
|
||||||
|
chewie: ^1.8.1
|
||||||
|
flutter_pdfview: ^1.3.2
|
||||||
|
permission_handler: ^11.3.0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
@@ -64,6 +78,8 @@ flutter:
|
|||||||
# included with your application, so that you can use the icons in
|
# included with your application, so that you can use the icons in
|
||||||
# the material Icons class.
|
# the material Icons class.
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
assets:
|
||||||
|
- assets/Images/K.png
|
||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
# To add assets to your application, add an assets section, like this:
|
||||||
# assets:
|
# assets:
|
||||||
|
|||||||
@@ -7,8 +7,17 @@
|
|||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <file_selector_windows/file_selector_windows.h>
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
|
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||||
|
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||||
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
FileSelectorWindowsRegisterWithRegistrar(
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
|
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||||
|
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||||
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
file_selector_windows
|
file_selector_windows
|
||||||
|
permission_handler_windows
|
||||||
|
share_plus
|
||||||
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
|||||||
Reference in New Issue
Block a user