Compare commits

10 Commits
Master ... main

Author SHA1 Message Date
Abhishek Mali
98d184d901 chat update 2026-02-27 10:12:51 +05:30
divya abdar
0c1d3b8cb2 changes in order track file 2025-12-25 10:26:47 +05:30
divya abdar
d419e4ed60 minor changes 2025-12-23 11:44:56 +05:30
Abhishek Mali
8dac57a3a8 status 2025-12-23 10:22:01 +05:30
Abhishek Mali
e85ac4bf8c download option in invoide 2025-12-19 10:48:19 +05:30
Abhishek Mali
d606156a6d update invoice section show download option 2025-12-18 12:45:26 +05:30
Abhishek Mali
b9fb9455e7 chat support download updated 2025-12-18 11:03:25 +05:30
Abhishek Mali
bbde34fae4 chat support update 2025-12-16 10:24:16 +05:30
Abhishek Mali
bb81269140 chat support 2025-12-15 11:10:52 +05:30
divya abdar
9faf983b95 Your changes 2025-12-11 18:36:11 +05:30
51 changed files with 6664 additions and 1069 deletions

File diff suppressed because one or more lines are too long

3
.fvmrc Normal file
View File

@@ -0,0 +1,3 @@
{
"flutter": "3.27.1"
}

3
.gitignore vendored
View File

@@ -32,3 +32,6 @@ windows/flutter/ephemeral/
# Logs # Logs
*.log *.log
# FVM Version Cache
.fvm/

View File

@@ -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"

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -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/";
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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,
);
} }
} }

View 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();
}
}

View File

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

View 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!),
),
);
}
}

View File

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

View File

@@ -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),
),
); );
} }
} }

View File

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

View File

@@ -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),
), ),
); );
} }

View File

@@ -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,
),
),
),
),
); );
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
} }

View File

@@ -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();
}

View File

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

View File

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

View File

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

View File

@@ -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,
),
);
}
} }

View File

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

View File

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

View File

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

View File

@@ -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',
}), }),

View 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();
}
}

View 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']);
}
}

View File

@@ -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) {

View File

@@ -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;
}
} }

View 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();
}
}

View File

@@ -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;
}
}
}

View 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")),
);
}
}
}

View 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";
}
}

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

View File

@@ -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,

View File

@@ -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),
),
),
), ),
); );
} }
} }

View File

@@ -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,
),
), ),
); );
} }

View File

@@ -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);
} }

View File

@@ -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

View File

@@ -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"))
} }

View File

@@ -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"

View File

@@ -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:

View File

@@ -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"));
} }

View File

@@ -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