Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98d184d901 | ||
|
|
0c1d3b8cb2 | ||
|
|
d419e4ed60 | ||
|
|
8dac57a3a8 | ||
|
|
e85ac4bf8c | ||
|
|
d606156a6d | ||
|
|
b9fb9455e7 | ||
|
|
bbde34fae4 | ||
|
|
bb81269140 | ||
|
|
9faf983b95 |
File diff suppressed because one or more lines are too long
3
.gitignore
vendored
3
.gitignore
vendored
@@ -32,3 +32,6 @@ windows/flutter/ephemeral/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# FVM Version Cache
|
||||
.fvm/
|
||||
@@ -4,11 +4,20 @@
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<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
|
||||
android:label="kent_logistics_app"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
|
||||
@@ -1,5 +1,37 @@
|
||||
package com.example.kent_logistics_app
|
||||
|
||||
import android.media.MediaScannerConnection
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
class MainActivity : FlutterActivity() {
|
||||
|
||||
private val CHANNEL = "media_scanner"
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
CHANNEL
|
||||
).setMethodCallHandler { call, result ->
|
||||
if (call.method == "scanFile") {
|
||||
val path = call.argument<String>("path")
|
||||
|
||||
if (path != null) {
|
||||
MediaScannerConnection.scanFile(
|
||||
applicationContext,
|
||||
arrayOf(path),
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
result.success(null)
|
||||
} else {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
assets/Images/K.png
Normal file
BIN
assets/Images/K.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -1,3 +1,8 @@
|
||||
// class ApiConfig {
|
||||
// static const String baseUrl = "http://103.248.30.24:3030/api";
|
||||
// static const String fileBaseUrl = "http://103.248.30.24:3030/";
|
||||
// }
|
||||
class ApiConfig {
|
||||
static const String baseUrl = "http://10.207.50.74:8000/api";
|
||||
static const String baseUrl = "http://10.119.0.74:8000/api";
|
||||
static const String fileBaseUrl = "http://10.119.0.74:8000/";
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ class AppConfig {
|
||||
static const String logoUrlEmulator = "http://10.0.2.2:8000/images/kent_logo2.png";
|
||||
|
||||
// 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?
|
||||
static const String logoUrl = logoUrlDevice; // CHANGE THIS WHEN TESTING ON REAL DEVICE
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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/invoice_provider.dart';
|
||||
import 'package:kent_logistics_app/providers/mark_list_provider.dart';
|
||||
@@ -30,6 +31,7 @@ void main() async {
|
||||
|
||||
),
|
||||
ChangeNotifierProvider(create: (_) => InvoiceProvider()),
|
||||
ChangeNotifierProvider(create: (_) => ChatUnreadProvider()),
|
||||
],
|
||||
child: const KentApp(),
|
||||
));
|
||||
@@ -54,7 +56,7 @@ class KentApp extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
ChangeNotifierProvider(create: (_) => InvoiceProvider()),
|
||||
|
||||
ChangeNotifierProvider(create: (_) => ChatUnreadProvider()),
|
||||
|
||||
],
|
||||
child: MaterialApp(
|
||||
@@ -64,7 +66,7 @@ class KentApp extends StatelessWidget {
|
||||
useMaterial3: true,
|
||||
textTheme: GoogleFonts.interTextTheme(),
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.red),
|
||||
scaffoldBackgroundColor: const Color(0xfff8f6ff), // your light background
|
||||
scaffoldBackgroundColor: const Color(0xFFE8F0FF), // your light background
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Colors.indigo, // FIX
|
||||
foregroundColor: Colors.white, // white text + icons
|
||||
|
||||
@@ -125,23 +125,27 @@ class AuthProvider extends ChangeNotifier {
|
||||
return res['success'] == true;
|
||||
}
|
||||
|
||||
// --------------------- REFRESH TOKEN --------------------------
|
||||
Future<bool> tryRefreshToken(BuildContext context) async {
|
||||
Future<void> forceLogout(BuildContext context) async {
|
||||
debugPrint('🚪🚪🚪 [AUTH] Force logout triggered');
|
||||
|
||||
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);
|
||||
notifyListeners();
|
||||
|
||||
if (res['success'] == true && res['token'] != null) {
|
||||
await prefs.setString('token', res['token']);
|
||||
_token = res['token'];
|
||||
notifyListeners();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
// Redirect to login & clear navigation stack
|
||||
Navigator.of(context).pushNamedAndRemoveUntil(
|
||||
'/login',
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
34
lib/providers/chat_unread_provider.dart
Normal file
34
lib/providers/chat_unread_provider.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ChatUnreadProvider extends ChangeNotifier {
|
||||
int _unreadCount = 0;
|
||||
bool _chatOpen = false;
|
||||
|
||||
int get unreadCount => _unreadCount;
|
||||
bool get isChatOpen => _chatOpen;
|
||||
|
||||
/// 📩 Called when ADMIN sends message
|
||||
void increment() {
|
||||
if (!_chatOpen) {
|
||||
_unreadCount++;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 👁 Called when chat screen is opened
|
||||
void setChatOpen(bool open) {
|
||||
_chatOpen = open;
|
||||
|
||||
if (open) {
|
||||
_unreadCount = 0; // reset badge when user opens chat
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 🔁 Manual reset (optional)
|
||||
void reset() {
|
||||
_unreadCount = 0;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,11 @@ import '../services/invoice_service.dart';
|
||||
|
||||
class InvoiceInstallmentScreen extends StatefulWidget {
|
||||
final int invoiceId;
|
||||
const InvoiceInstallmentScreen({super.key, required this.invoiceId});
|
||||
|
||||
const InvoiceInstallmentScreen({
|
||||
super.key,
|
||||
required this.invoiceId,
|
||||
});
|
||||
|
||||
@override
|
||||
State<InvoiceInstallmentScreen> createState() =>
|
||||
@@ -35,39 +39,188 @@ class _InvoiceInstallmentScreenState extends State<InvoiceInstallmentScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Installments")),
|
||||
backgroundColor: Colors.grey.shade100,
|
||||
appBar: AppBar(
|
||||
title: const Text("Installments"),
|
||||
elevation: 1,
|
||||
),
|
||||
body: loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: installments.isEmpty
|
||||
? const Center(
|
||||
child: Text("Installments not created yet",
|
||||
style: TextStyle(fontSize: 18)),
|
||||
)
|
||||
? _buildEmptyState()
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: width * 0.04,
|
||||
vertical: 16,
|
||||
),
|
||||
itemCount: installments.length,
|
||||
itemBuilder: (_, i) {
|
||||
final inst = installments[i];
|
||||
return Card(
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
"Amount: ₹${inst['amount']?.toString() ?? '0'}"),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Date: ${inst['installment_date'] ?? 'N/A'}"),
|
||||
Text(
|
||||
"Payment: ${inst['payment_method'] ?? 'N/A'}"),
|
||||
Text(
|
||||
"Reference: ${inst['reference_no'] ?? 'N/A'}"),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
return InstallmentCard(inst: installments[i]);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
children: [
|
||||
// Amount + Payment Method Row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"₹${getString('amount')}",
|
||||
style: TextStyle(
|
||||
fontSize: amountSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
),
|
||||
|
||||
// Payment Chip
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: isTablet ? 8 : 6,
|
||||
horizontal: isTablet ? 16 : 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
child: Text(
|
||||
getString('payment_method'),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.blue.shade700,
|
||||
fontSize: isTablet ? 15 : 13.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: isTablet ? 24 : 18),
|
||||
|
||||
// Responsive Info Rows
|
||||
buildInfoRow(
|
||||
Icons.calendar_month,
|
||||
"Date",
|
||||
getString("installment_date"),
|
||||
isTablet
|
||||
),
|
||||
SizedBox(height: isTablet ? 14 : 10),
|
||||
buildInfoRow(
|
||||
Icons.confirmation_number,
|
||||
"Reference",
|
||||
getString("reference_no"),
|
||||
isTablet
|
||||
),
|
||||
|
||||
SizedBox(height: isTablet ? 24 : 18),
|
||||
Divider(color: Colors.grey.shade300, thickness: 1),
|
||||
SizedBox(height: isTablet ? 10 : 6),
|
||||
|
||||
// Align(
|
||||
// alignment: Alignment.centerRight,
|
||||
// child: Text(
|
||||
// "Installment #${inst['id'] ?? ''}",
|
||||
// style: TextStyle(
|
||||
// fontSize: isTablet ? 15 : 13,
|
||||
// color: Colors.grey.shade600,
|
||||
// fontWeight: FontWeight.w500,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Responsive info row builder
|
||||
Widget buildInfoRow(IconData icon, String label, String value, bool isTablet) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: isTablet ? 24 : 20, color: Colors.grey.shade600),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
"$label:",
|
||||
style: TextStyle(
|
||||
fontSize: isTablet ? 17 : 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: isTablet ? 16 : 15,
|
||||
color: Colors.grey.shade800,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
145
lib/screens/chat_file_viewer.dart
Normal file
145
lib/screens/chat_file_viewer.dart
Normal file
@@ -0,0 +1,145 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:flutter_pdfview/flutter_pdfview.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
class ChatFileViewer {
|
||||
/// Entry point used by chat screen
|
||||
static void open(
|
||||
BuildContext context, {
|
||||
required String url,
|
||||
required String fileType,
|
||||
}) {
|
||||
if (fileType.startsWith('image/')) {
|
||||
_openImage(context, url);
|
||||
} else if (fileType.startsWith('video/')) {
|
||||
_openVideo(context, url);
|
||||
} else if (fileType == 'application/pdf') {
|
||||
_openPdf(context, url);
|
||||
} else {
|
||||
_downloadFile(context, url);
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// IMAGE VIEWER
|
||||
// ===========================
|
||||
static void _openImage(BuildContext context, String url) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(backgroundColor: Colors.black),
|
||||
body: PhotoView(
|
||||
imageProvider: NetworkImage(url),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// VIDEO VIEWER
|
||||
// ===========================
|
||||
static void _openVideo(BuildContext context, String url) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => _VideoPlayerPage(url: url),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// PDF VIEWER
|
||||
// ===========================
|
||||
static Future<void> _openPdf(
|
||||
BuildContext context, String url) async {
|
||||
final dir = await getTemporaryDirectory();
|
||||
final path = "${dir.path}/chat.pdf";
|
||||
|
||||
await Dio().download(url, path);
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => Scaffold(
|
||||
appBar: AppBar(title: const Text("PDF")),
|
||||
body: PDFView(filePath: path),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// FILE DOWNLOAD
|
||||
// ===========================
|
||||
static Future<void> _downloadFile(
|
||||
BuildContext context, String url) async {
|
||||
final dir = await getExternalStorageDirectory();
|
||||
final fileName = url.split('/').last;
|
||||
final path = "${dir!.path}/$fileName";
|
||||
|
||||
await Dio().download(url, path);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("File downloaded: $fileName")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// VIDEO PLAYER WIDGET
|
||||
// ===========================
|
||||
class _VideoPlayerPage extends StatefulWidget {
|
||||
final String url;
|
||||
const _VideoPlayerPage({required this.url});
|
||||
|
||||
@override
|
||||
State<_VideoPlayerPage> createState() => _VideoPlayerPageState();
|
||||
}
|
||||
|
||||
class _VideoPlayerPageState extends State<_VideoPlayerPage> {
|
||||
late VideoPlayerController _videoController;
|
||||
ChewieController? _chewieController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_videoController = VideoPlayerController.network(widget.url)
|
||||
..initialize().then((_) {
|
||||
setState(() {
|
||||
_chewieController = ChewieController(
|
||||
videoPlayerController: _videoController,
|
||||
autoPlay: true,
|
||||
looping: false,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_videoController.dispose();
|
||||
_chewieController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Video")),
|
||||
body: Center(
|
||||
child: _chewieController == null
|
||||
? const CircularProgressIndicator()
|
||||
: Chewie(controller: _chewieController!),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,386 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package: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});
|
||||
|
||||
@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
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(
|
||||
child: Text("Invoice Content He", style: TextStyle(fontSize: 18)),
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Support Chat")),
|
||||
body: isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Column(
|
||||
children: [
|
||||
Expanded(child: _buildMessages()),
|
||||
_buildInput(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessageContent({
|
||||
String? message,
|
||||
String? filePath,
|
||||
String? fileType,
|
||||
required bool isUser,
|
||||
}) {
|
||||
final textColor = isUser ? Colors.white : Colors.black;
|
||||
|
||||
if (filePath == null) {
|
||||
return Text(message ?? '', style: TextStyle(color: textColor));
|
||||
}
|
||||
|
||||
final url = "${DioClient.baseUrl}/storage/$filePath";
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
ChatFileViewer.open(
|
||||
context,
|
||||
url: url,
|
||||
fileType: fileType ?? '',
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(_iconForFile(fileType), color: textColor),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
_labelForFile(fileType),
|
||||
style: TextStyle(color: textColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
IconData _iconForFile(String? type) {
|
||||
if (type == null) return Icons.insert_drive_file;
|
||||
if (type.startsWith('image/')) return Icons.image;
|
||||
if (type.startsWith('video/')) return Icons.play_circle_fill;
|
||||
if (type == 'application/pdf') return Icons.picture_as_pdf;
|
||||
return Icons.insert_drive_file;
|
||||
}
|
||||
|
||||
String _labelForFile(String? type) {
|
||||
if (type == null) return "File";
|
||||
if (type.startsWith('image/')) return "Image";
|
||||
if (type.startsWith('video/')) return "Video";
|
||||
if (type == 'application/pdf') return "PDF";
|
||||
return "Download file";
|
||||
}
|
||||
|
||||
Widget _buildMessages() {
|
||||
return ListView(
|
||||
controller: _scrollCtrl,
|
||||
padding: const EdgeInsets.all(12),
|
||||
children: [
|
||||
// EXISTING MESSAGES
|
||||
...messages.map((msg) {
|
||||
final isUser = msg['sender_type'] == 'App\\Models\\User';
|
||||
|
||||
return Align(
|
||||
alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 6),
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: isUser ? Colors.blue : Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: msg['file_path'] == null
|
||||
? Text(
|
||||
msg['message'] ?? '',
|
||||
style: TextStyle(
|
||||
color: isUser ? Colors.white : Colors.black,
|
||||
),
|
||||
)
|
||||
: ChatFilePreview(
|
||||
filePath: msg['file_path'],
|
||||
fileType: msg['file_type'] ?? '',
|
||||
isUser: isUser,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
// ⏳ UPLOADING MESSAGE
|
||||
// ⏳ UPLOADING MESSAGE (SAFE & NO DUPLICATE)
|
||||
if (uploadingMessage != null)
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 6),
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// ✅ SHOW PREVIEW ONLY FOR IMAGE / VIDEO
|
||||
if (uploadingMessage!['file_type'].startsWith('image/') ||
|
||||
uploadingMessage!['file_type'].startsWith('video/'))
|
||||
ChatFilePreview(
|
||||
filePath: uploadingMessage!['local_file'].path,
|
||||
fileType: uploadingMessage!['file_type'],
|
||||
isUser: true,
|
||||
isLocal: true,
|
||||
)
|
||||
else
|
||||
// 📄 DOCUMENT PLACEHOLDER
|
||||
const Icon(
|
||||
Icons.insert_drive_file,
|
||||
color: Colors.white,
|
||||
size: 40,
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
LinearProgressIndicator(
|
||||
value: uploadingMessage!['progress'],
|
||||
backgroundColor: Colors.white24,
|
||||
valueColor:
|
||||
const AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
|
||||
const Text(
|
||||
"Sending…",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInput() {
|
||||
return SafeArea(
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.attach_file),
|
||||
onPressed: _pickAndSendFile,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _messageCtrl,
|
||||
decoration: const InputDecoration(
|
||||
hintText: "Type message",
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.send),
|
||||
onPressed: _sendMessage,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,249 +12,585 @@ class DashboardScreen extends StatefulWidget {
|
||||
State<DashboardScreen> createState() => _DashboardScreenState();
|
||||
}
|
||||
|
||||
class _DashboardScreenState extends State<DashboardScreen> {
|
||||
class _DashboardScreenState extends State<DashboardScreen>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _scaleCtrl;
|
||||
late AnimationController _shineCtrl;
|
||||
|
||||
@override
|
||||
void 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 {
|
||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||
|
||||
// STEP 1: Try refresh token BEFORE any API calls
|
||||
await auth.tryRefreshToken(context);
|
||||
|
||||
// STEP 2: Now safe to load dashboard
|
||||
final dash = Provider.of<DashboardProvider>(context, listen: false);
|
||||
dash.init(context);
|
||||
await dash.loadSummary(context);
|
||||
|
||||
// STEP 3: Load marks AFTER refresh
|
||||
final marks = Provider.of<MarkListProvider>(context, listen: false);
|
||||
marks.init(context);
|
||||
await marks.loadMarks(context);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scaleCtrl.dispose();
|
||||
_shineCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _showAddMarkForm() {
|
||||
// ============================================================
|
||||
// CENTERED ADD MARK POPUP
|
||||
// ============================================================
|
||||
void _showAddMarkForm(double scale) {
|
||||
final markCtrl = TextEditingController();
|
||||
final originCtrl = TextEditingController();
|
||||
final destCtrl = TextEditingController();
|
||||
|
||||
showModalBottomSheet(
|
||||
showDialog(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
|
||||
barrierDismissible: true,
|
||||
builder: (_) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
18,
|
||||
18,
|
||||
18,
|
||||
MediaQuery.of(context).viewInsets.bottom + 20,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
"Add Mark No",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
return Center(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width * 0.88,
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"Add Mark No",
|
||||
style: TextStyle(
|
||||
fontSize: 20 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.indigo,
|
||||
),
|
||||
),
|
||||
|
||||
TextField(controller: markCtrl, decoration: const InputDecoration(labelText: "Mark No")),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(height: 18 * scale),
|
||||
|
||||
TextField(controller: originCtrl, decoration: const InputDecoration(labelText: "Origin")),
|
||||
const SizedBox(height: 12),
|
||||
_inputField(markCtrl, "Mark No", scale),
|
||||
SizedBox(height: 12 * scale),
|
||||
_inputField(originCtrl, "Origin", scale),
|
||||
SizedBox(height: 12 * scale),
|
||||
_inputField(destCtrl, "Destination", scale),
|
||||
|
||||
TextField(controller: destCtrl, decoration: const InputDecoration(labelText: "Destination")),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(height: 22 * scale),
|
||||
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final mark = markCtrl.text.trim();
|
||||
final origin = originCtrl.text.trim();
|
||||
final dest = destCtrl.text.trim();
|
||||
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 {
|
||||
final mark = markCtrl.text.trim();
|
||||
final origin = originCtrl.text.trim();
|
||||
final dest = destCtrl.text.trim();
|
||||
|
||||
if (mark.isEmpty || origin.isEmpty || dest.isEmpty) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(content: Text("All fields required")));
|
||||
return;
|
||||
}
|
||||
if (mark.isEmpty || origin.isEmpty || dest.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("All fields are required")),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final provider = Provider.of<MarkListProvider>(context, listen: false);
|
||||
final res = await provider.addMark(context, mark, origin, dest);
|
||||
final provider =
|
||||
Provider.of<MarkListProvider>(context, listen: false);
|
||||
final res =
|
||||
await provider.addMark(context, mark, origin, dest);
|
||||
|
||||
if (res['success'] == true) {
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
final msg = res['message'] ?? "Failed";
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||
}
|
||||
},
|
||||
child: const Text("Submit"),
|
||||
if (res['success'] == true) {
|
||||
await Provider.of<MarkListProvider>(context,
|
||||
listen: false)
|
||||
.loadMarks(context);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(res['message'] ?? "Failed")),
|
||||
);
|
||||
}
|
||||
},
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final auth = Provider.of<AuthProvider>(context);
|
||||
final dash = Provider.of<DashboardProvider>(context);
|
||||
final marks = Provider.of<MarkListProvider>(context);
|
||||
|
||||
final name = auth.user?['customer_name'] ?? 'User';
|
||||
|
||||
if (dash.loading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final name = auth.user?['customer_name'] ?? "User";
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final scale = (width / 430).clamp(0.88, 1.08);
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(18),
|
||||
padding: EdgeInsets.all(16 * scale),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// HEADER
|
||||
Text(
|
||||
"Welcome, $name 👋",
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600),
|
||||
// WELCOME CARD WITH ANIMATION
|
||||
AnimatedBuilder(
|
||||
animation: _scaleCtrl,
|
||||
builder: (_, __) {
|
||||
return Transform.scale(
|
||||
scale: _scaleCtrl.value,
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(20 * scale),
|
||||
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),
|
||||
|
||||
// ORDER SUMMARY
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_statBox("Active", dash.activeOrders, Colors.blue),
|
||||
_statBox("In Transit", dash.inTransitOrders, Colors.orange),
|
||||
_statBox("Delivered", dash.deliveredOrders, Colors.green),
|
||||
],
|
||||
SizedBox(height: 18 * scale),
|
||||
|
||||
_summarySection(
|
||||
dash,
|
||||
rawAmount: "₹${dash.totalRaw ?? 0}",
|
||||
scale: scale,
|
||||
),
|
||||
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: 26 * scale),
|
||||
|
||||
// ADD + VIEW ALL BUTTONS SIDE BY SIDE
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text("Add Mark No"),
|
||||
onPressed: _showAddMarkForm,
|
||||
),
|
||||
|
||||
if (marks.marks.length > 0)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const MarkListScreen()),
|
||||
);
|
||||
},
|
||||
child: const Text(
|
||||
"View All →",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.indigo,
|
||||
fontWeight: FontWeight.w600,
|
||||
// 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),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Latest Mark Numbers",
|
||||
style: TextStyle(
|
||||
fontSize: 18 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (marks.marks.isNotEmpty)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const MarkListScreen()),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
"View All →",
|
||||
style: TextStyle(
|
||||
fontSize: 15 * scale,
|
||||
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),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(height: 12 * scale),
|
||||
|
||||
// MARK LIST (only 10 latest)
|
||||
const Text(
|
||||
"Latest Mark Numbers",
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
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),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(height: 16 * scale),
|
||||
|
||||
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),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
_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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Icon(icon, size: 28 * scale, color: color),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _rawAmountTile(String title, String value, double scale) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(14 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.deepPurple.shade50,
|
||||
borderRadius: BorderRadius.circular(14 * scale),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 20 * scale,
|
||||
color: Colors.deepPurple,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// UI WIDGETS
|
||||
Widget _statBox(String title, int value, Color color) {
|
||||
return Container(
|
||||
width: 110,
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(value.toString(),
|
||||
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
Text(title, style: const TextStyle(fontSize: 14)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _valueCard(String title, String value) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.indigo.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14, fontWeight: FontWeight.w600, color: Colors.grey)),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.indigo,
|
||||
),
|
||||
),
|
||||
],
|
||||
// ============================================================
|
||||
// INPUT FIELD
|
||||
// ============================================================
|
||||
Widget _inputField(TextEditingController controller, String label, double scale) {
|
||||
return TextField(
|
||||
controller: controller,
|
||||
style: TextStyle(fontSize: 15 * scale),
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
labelStyle: TextStyle(fontSize: 14 * scale),
|
||||
filled: true,
|
||||
fillColor: Colors.lightBlue.shade50,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12 * scale),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 14 * scale, vertical: 14 * scale),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final profile = Provider.of<UserProfileProvider>(
|
||||
context, listen: false).profile;
|
||||
final profile =
|
||||
Provider.of<UserProfileProvider>(context, listen: false).profile;
|
||||
|
||||
nameCtrl.text = profile?.customerName ?? '';
|
||||
companyCtrl.text = profile?.companyName ?? '';
|
||||
@@ -32,8 +32,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
final provider =
|
||||
Provider.of<UserProfileProvider>(context, listen: false);
|
||||
final provider = Provider.of<UserProfileProvider>(context, listen: false);
|
||||
|
||||
final data = {
|
||||
"customer_name": nameCtrl.text,
|
||||
@@ -44,14 +43,16 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||
"pincode": pincodeCtrl.text,
|
||||
};
|
||||
|
||||
final success =
|
||||
await provider.sendProfileUpdateRequest(context, data);
|
||||
final success = await provider.sendProfileUpdateRequest(context, data);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(success
|
||||
content: Text(
|
||||
success
|
||||
? "Request submitted. Wait for admin approval."
|
||||
: "Failed to submit request")),
|
||||
: "Failed to submit request",
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (success) Navigator.pop(context);
|
||||
@@ -59,38 +60,179 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Edit Profile")),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(18),
|
||||
child: Column(
|
||||
children: [
|
||||
_field("Name", nameCtrl),
|
||||
_field("Company", companyCtrl),
|
||||
_field("Email", emailCtrl),
|
||||
_field("Mobile", mobileCtrl),
|
||||
_field("Address", addressCtrl),
|
||||
_field("Pincode", pincodeCtrl),
|
||||
final darkBlue = const Color(0xFF003B73);
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: _submit,
|
||||
child: const Text("Submit Update Request"),
|
||||
)
|
||||
],
|
||||
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(
|
||||
children: [
|
||||
_buildField("Full Name", nameCtrl, scale),
|
||||
_buildField("Company Name", companyCtrl, scale),
|
||||
_buildField("Email Address", emailCtrl, scale),
|
||||
_buildField("Mobile Number", mobileCtrl, scale),
|
||||
_buildField("Address", addressCtrl, scale),
|
||||
_buildField("Pincode", pincodeCtrl, scale),
|
||||
|
||||
SizedBox(height: 25 * scale),
|
||||
|
||||
/// 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,
|
||||
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(
|
||||
padding: const EdgeInsets.only(bottom: 14),
|
||||
child: TextField(
|
||||
controller: ctrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: title,
|
||||
border: OutlineInputBorder(),
|
||||
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(
|
||||
controller: ctrl,
|
||||
style: TextStyle(fontSize: 15 * scale),
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
labelText: label,
|
||||
labelStyle: TextStyle(
|
||||
color: const Color(0xFF003B73),
|
||||
fontSize: 14 * scale,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16 * scale),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16 * scale),
|
||||
borderSide: BorderSide(
|
||||
color: const Color(0xFF003B73),
|
||||
width: 2 * scale,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/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/invoice_service.dart';
|
||||
import '../widgets/invoice_detail_view.dart';
|
||||
|
||||
class InvoiceDetailScreen extends StatefulWidget {
|
||||
final int invoiceId;
|
||||
@@ -22,148 +31,148 @@ class _InvoiceDetailScreenState extends State<InvoiceDetailScreen> {
|
||||
|
||||
Future<void> load() async {
|
||||
final service = InvoiceService(DioClient.getInstance(context));
|
||||
final res = await service.getInvoiceDetails(widget.invoiceId);
|
||||
try {
|
||||
final res = await service.getInvoiceDetails(widget.invoiceId);
|
||||
|
||||
if (res['success'] == true) {
|
||||
invoice = res['invoice'] ?? {};
|
||||
if (res['success'] == true) {
|
||||
invoice = res['invoice'] ?? {};
|
||||
} else {
|
||||
invoice = {};
|
||||
}
|
||||
} catch (e) {
|
||||
// handle error gracefully
|
||||
invoice = {};
|
||||
} finally {
|
||||
if (mounted) setState(() => loading = false);
|
||||
}
|
||||
|
||||
loading = false;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
/// ---------- REUSABLE ROW ----------
|
||||
Widget row(String label, dynamic value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label, style: const TextStyle(fontSize: 14, color: Colors.grey)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value?.toString().isNotEmpty == true ? value.toString() : "N/A",
|
||||
textAlign: TextAlign.end,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
String? get pdfUrl {
|
||||
final path = invoice['pdf_path'];
|
||||
if (path == null || path.toString().isEmpty) return null;
|
||||
|
||||
return ApiConfig.fileBaseUrl + path;
|
||||
}
|
||||
|
||||
static const MethodChannel _mediaScanner =
|
||||
MethodChannel('media_scanner');
|
||||
|
||||
Future<void> _scanFile(String path) async {
|
||||
try {
|
||||
await _mediaScanner.invokeMethod('scanFile', {'path': path});
|
||||
} catch (e) {
|
||||
debugPrint("❌ MediaScanner error: $e");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Future<File?> _downloadPdf(String url) async {
|
||||
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
|
||||
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(
|
||||
appBar: AppBar(title: const Text("Invoice Details")),
|
||||
appBar: AppBar(
|
||||
title: const Text("Invoice Details"),
|
||||
actions: pdfUrl == null
|
||||
? []
|
||||
: [
|
||||
// DOWNLOAD
|
||||
IconButton(
|
||||
icon: const Icon(Icons.download_rounded),
|
||||
onPressed: () async {
|
||||
final file = await _downloadPdf(pdfUrl!);
|
||||
if (file != null && mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("Invoice downloaded")),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
// 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 DATA ================
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ListView(
|
||||
children: [
|
||||
/// -------- Invoice Summary --------
|
||||
const Text(
|
||||
"Invoice Summary",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
row("Invoice No", invoice['invoice_number']),
|
||||
row("Invoice Date", invoice['invoice_date']),
|
||||
row("Due Date", invoice['due_date']),
|
||||
row("Status", invoice['status']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
/// -------- Customer Details --------
|
||||
const Text(
|
||||
"Customer Details",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
row("Name", invoice['customer_name']),
|
||||
row("Company", invoice['company_name']),
|
||||
row("Email", invoice['customer_email']),
|
||||
row("Mobile", invoice['customer_mobile']),
|
||||
row("Address", invoice['customer_address']),
|
||||
row("Pincode", invoice['pincode']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
/// -------- Amounts & Taxes --------
|
||||
const Text(
|
||||
"Amounts",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
row("Final Amount", invoice['final_amount']),
|
||||
row("Tax Type", invoice['tax_type']),
|
||||
row("GST %", invoice['gst_percent']),
|
||||
row("GST Amount", invoice['gst_amount']),
|
||||
row("Final with GST", invoice['final_amount_with_gst']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
/// -------- Payment Details --------
|
||||
const Text(
|
||||
"Payment Details",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
row("Payment Method", invoice['payment_method']),
|
||||
row("Reference No", invoice['reference_no']),
|
||||
row("Notes", invoice['notes']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
/// -------- PDF --------
|
||||
if (invoice['pdf_path'] != null)
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.picture_as_pdf),
|
||||
label: const Text("Download PDF"),
|
||||
onPressed: () {},
|
||||
style:
|
||||
ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
/// -------- Invoice Items --------
|
||||
if (invoice['items'] != null)
|
||||
const Text(
|
||||
"Invoice Items",
|
||||
style:
|
||||
TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
...List.generate(invoice['items']?.length ?? 0, (i) {
|
||||
final item = invoice['items'][i];
|
||||
return Card(
|
||||
child: ListTile(
|
||||
title: Text(item['description'] ?? "Item"),
|
||||
subtitle: Text("Qty: ${item['qty'] ?? 0}"),
|
||||
trailing: Text(
|
||||
"₹${item['ttl_amount'] ?? 0}",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.indigo),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
: invoice.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
"No Invoice Data Found",
|
||||
style: TextStyle(
|
||||
fontSize: 18 * scale,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding: EdgeInsets.all(12 * scale),
|
||||
// NOTE: InvoiceDetailView should handle its own responsiveness.
|
||||
// If you want it to follow the same `scale`, I can update that widget to accept `scale`.
|
||||
child: InvoiceDetailView(invoice: invoice),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/invoice_installment_screen.dart';
|
||||
import '../providers/invoice_provider.dart';
|
||||
import '../services/dio_client.dart';
|
||||
import '../services/invoice_service.dart';
|
||||
import 'invoice_detail_screen.dart';
|
||||
|
||||
|
||||
class InvoiceScreen extends StatefulWidget {
|
||||
const InvoiceScreen({super.key});
|
||||
|
||||
@@ -15,10 +12,11 @@ class InvoiceScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _InvoiceScreenState extends State<InvoiceScreen> {
|
||||
String searchQuery = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
Provider.of<InvoiceProvider>(context, listen: false)
|
||||
.loadInvoices(context);
|
||||
@@ -29,78 +27,263 @@ class _InvoiceScreenState extends State<InvoiceScreen> {
|
||||
Widget build(BuildContext 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) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (provider.invoices.isEmpty) {
|
||||
return const Center(
|
||||
child: Text("No invoices found", style: TextStyle(fontSize: 18)));
|
||||
}
|
||||
// 🔍 Filter invoices based on search query
|
||||
final filteredInvoices = provider.invoices.where((inv) {
|
||||
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(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: provider.invoices.length,
|
||||
itemBuilder: (_, i) {
|
||||
final inv = provider.invoices[i];
|
||||
return Column(
|
||||
children: [
|
||||
// 🔍 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Invoice ${inv['invoice_number'] ?? 'N/A'}",
|
||||
style: const TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.bold),
|
||||
// 📄 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) {
|
||||
final inv = filteredInvoices[i];
|
||||
|
||||
return Card(
|
||||
color: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16 * scale),
|
||||
),
|
||||
|
||||
const SizedBox(height: 6),
|
||||
Text("Date: ${inv['invoice_date'] ?? 'N/A'}"),
|
||||
Text("Status: ${inv['status'] ?? 'N/A'}"),
|
||||
Text("Amount: ₹${inv['formatted_amount'] ?? '0'}"),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
elevation: 3,
|
||||
margin: EdgeInsets.only(bottom: 14 * scale),
|
||||
child: Stack(
|
||||
children: [
|
||||
OutlinedButton(
|
||||
child: const Text("Invoice Details"),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => InvoiceDetailScreen(
|
||||
invoiceId: inv['invoice_id'],
|
||||
Padding(
|
||||
padding: EdgeInsets.all(16 * scale),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
/// Invoice Number
|
||||
Text(
|
||||
"Invoice ${inv['invoice_number'] ?? 'N/A'}",
|
||||
style: TextStyle(
|
||||
fontSize: 20 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
SizedBox(height: 8 * scale),
|
||||
|
||||
/// 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(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GradientButton(
|
||||
text: "Invoice Details",
|
||||
fontSize: 15 * scale,
|
||||
radius: 12 * scale,
|
||||
padding: 14 * scale,
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
Color(0xFF1976D2),
|
||||
Color(0xFF42A5F5),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) =>
|
||||
InvoiceDetailScreen(
|
||||
invoiceId:
|
||||
inv['invoice_id']),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(width: 12 * scale),
|
||||
|
||||
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(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) =>
|
||||
InvoiceInstallmentScreen(
|
||||
invoiceId:
|
||||
inv['invoice_id']),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
OutlinedButton(
|
||||
child: const Text("Installments"),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => InvoiceInstallmentScreen(
|
||||
invoiceId: inv['invoice_id'],
|
||||
),
|
||||
/// STATUS BADGE
|
||||
Positioned(
|
||||
right: 12 * scale,
|
||||
top: 12 * scale,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 10 * scale,
|
||||
vertical: 6 * scale,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatusColor(inv['status']),
|
||||
borderRadius: BorderRadius.circular(12 * scale),
|
||||
),
|
||||
child: Text(
|
||||
inv['status'] ?? 'N/A',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Status Color Helper
|
||||
Color _getStatusColor(String? status) {
|
||||
switch (status?.toLowerCase()) {
|
||||
case 'pending':
|
||||
return Colors.orange;
|
||||
case 'in transit':
|
||||
return Colors.blue;
|
||||
case 'overdue':
|
||||
return Colors.redAccent;
|
||||
case 'paid':
|
||||
return Colors.green;
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
/// -------------------------------------------------------
|
||||
/// RESPONSIVE GRADIENT BUTTON
|
||||
/// -------------------------------------------------------
|
||||
class GradientButton extends StatelessWidget {
|
||||
final String text;
|
||||
final Gradient gradient;
|
||||
final VoidCallback onTap;
|
||||
|
||||
final double fontSize;
|
||||
final double padding;
|
||||
final double radius;
|
||||
|
||||
const GradientButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.gradient,
|
||||
required this.onTap,
|
||||
required this.fontSize,
|
||||
required this.padding,
|
||||
required this.radius,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
onTap: onTap,
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
gradient: gradient,
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(vertical: padding),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,27 +26,27 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
Future<void> _login() async {
|
||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||
|
||||
final loginId = cLoginId.text.trim();
|
||||
final password = cPassword.text.trim();
|
||||
final id = cLoginId.text.trim();
|
||||
final pass = cPassword.text.trim();
|
||||
|
||||
if (loginId.isEmpty || password.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Please enter login id and password')),
|
||||
);
|
||||
if (id.isEmpty || pass.isEmpty) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(content: Text("Please fill all fields")));
|
||||
return;
|
||||
}
|
||||
|
||||
final res = await auth.login(context, loginId, password);
|
||||
final res = await auth.login(context, id, pass);
|
||||
|
||||
if (res['success'] == true) {
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pushReplacement(
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const MainBottomNav()),
|
||||
);
|
||||
} else {
|
||||
final msg = res['message']?.toString() ?? 'Login failed';
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(res['message'] ?? "Login failed")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,38 +55,113 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
final auth = Provider.of<AuthProvider>(context);
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const WelcomeScreen()),
|
||||
);
|
||||
},
|
||||
),
|
||||
title: const Text('Login'),
|
||||
),
|
||||
/// ⭐ RESPONSIVE SCALE
|
||||
final scale = (width / 390).clamp(0.85, 1.25);
|
||||
|
||||
body: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: width * 0.06, vertical: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
RoundedInput(
|
||||
controller: cLoginId,
|
||||
hint: 'Email / Mobile / Customer ID',
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFE8F0FF),
|
||||
|
||||
body: Stack(
|
||||
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(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const WelcomeScreen()),
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(10 * scale),
|
||||
child: Icon(Icons.arrow_back,
|
||||
color: Colors.white, size: 20 * scale),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
RoundedInput(
|
||||
controller: cPassword,
|
||||
hint: 'Password',
|
||||
obscure: true,
|
||||
),
|
||||
|
||||
/// 📦 Center White Card (Responsive)
|
||||
Center(
|
||||
child: Container(
|
||||
width: width * 0.87,
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 28 * scale,
|
||||
horizontal: 20 * scale,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(22 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
blurRadius: 18 * scale,
|
||||
spreadRadius: 1,
|
||||
offset: Offset(0, 6 * scale),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
PrimaryButton(label: 'Login', onTap: _login, busy: auth.loading),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ import 'order_screen.dart';
|
||||
import 'invoice_screen.dart';
|
||||
import 'chat_screen.dart';
|
||||
import 'settings_screen.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/chat_unread_provider.dart';
|
||||
|
||||
|
||||
class MainBottomNav extends StatefulWidget {
|
||||
const MainBottomNav({super.key});
|
||||
@@ -17,12 +20,10 @@ class MainBottomNavState extends State<MainBottomNav> {
|
||||
int _currentIndex = 0;
|
||||
|
||||
void setIndex(int index) {
|
||||
setState(() {
|
||||
_currentIndex = index;
|
||||
});
|
||||
setState(() => _currentIndex = index);
|
||||
}
|
||||
|
||||
final List<Widget> _screens = const [
|
||||
final List<Widget> _screens = [
|
||||
DashboardScreen(),
|
||||
OrdersScreen(),
|
||||
InvoiceScreen(),
|
||||
@@ -30,26 +31,209 @@ class MainBottomNavState extends State<MainBottomNav> {
|
||||
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
|
||||
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(
|
||||
appBar: const MainAppBar(),
|
||||
body: _screens[_currentIndex],
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
currentIndex: _currentIndex,
|
||||
selectedItemColor: Colors.red,
|
||||
unselectedItemColor: Colors.black,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
onTap: (index) {
|
||||
setState(() => _currentIndex = index);
|
||||
},
|
||||
items: const [
|
||||
BottomNavigationBarItem(icon: Icon(Icons.dashboard_outlined), label: "Dashboard"),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.shopping_bag_outlined), label: "Orders"),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.receipt_long_outlined), label: "Invoice"),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.chat_bubble_outline), label: "Chat"),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.settings_outlined), label: "Settings"),
|
||||
],
|
||||
|
||||
bottomNavigationBar: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 10 * scale,
|
||||
right: 10 * scale,
|
||||
bottom: 10 * scale,
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final totalWidth = constraints.maxWidth;
|
||||
|
||||
// inner width (after padding)
|
||||
final contentWidth = totalWidth - (containerPadding * 2);
|
||||
|
||||
final safeContentWidth =
|
||||
contentWidth > 0 ? contentWidth : totalWidth;
|
||||
|
||||
final itemWidth = safeContentWidth / _icons.length;
|
||||
|
||||
final indicatorWidth = 70 * scale;
|
||||
final indicatorHeight = 70 * scale;
|
||||
|
||||
double left = (_currentIndex * itemWidth) +
|
||||
(itemWidth / 2) -
|
||||
(indicatorWidth / 2);
|
||||
|
||||
/// ⭐ FIX: explicitly convert clamp to double
|
||||
final double safeLeft = left
|
||||
.clamp(0, safeContentWidth - indicatorWidth)
|
||||
.toDouble();
|
||||
|
||||
return Container(
|
||||
height: 100 * scale,
|
||||
padding: EdgeInsets.symmetric(horizontal: containerPadding),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(28 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 20 * scale,
|
||||
offset: Offset(0, 8 * scale),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
/// ⭐ Indicator - safe positioned
|
||||
AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
curve: Curves.easeOut,
|
||||
top: 10 * scale,
|
||||
left: safeLeft,
|
||||
child: Container(
|
||||
width: indicatorWidth,
|
||||
height: indicatorHeight,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
Color(0xFF4F46E5),
|
||||
Color(0xFF06B6D4),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20 * scale),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
_icons[_currentIndex],
|
||||
size: 22 * scale,
|
||||
color: Colors.white,
|
||||
),
|
||||
SizedBox(height: 2 * scale),
|
||||
Text(
|
||||
_labels[_currentIndex],
|
||||
style: TextStyle(
|
||||
fontSize: 10 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
/// ⭐ Icon Row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: List.generate(_icons.length, (index) {
|
||||
final selected = index == _currentIndex;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => setIndex(index),
|
||||
child: SizedBox(
|
||||
width: itemWidth,
|
||||
height: 100 * scale,
|
||||
child: Center(
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
opacity: selected ? 0 : 1,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
index == 3
|
||||
? Consumer<ChatUnreadProvider>(
|
||||
builder: (_, chat, __) {
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Icon(
|
||||
_icons[index],
|
||||
size: 22 * scale,
|
||||
color: Colors.black87,
|
||||
),
|
||||
|
||||
if (chat.unreadCount > 0)
|
||||
Positioned(
|
||||
right: -6,
|
||||
top: -6,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.red,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 20,
|
||||
minHeight: 20,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
chat.unreadCount.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
: Icon(
|
||||
_icons[index],
|
||||
size: 22 * scale,
|
||||
color: Colors.black87,
|
||||
),
|
||||
|
||||
SizedBox(height: 4 * scale),
|
||||
Text(
|
||||
_labels[index],
|
||||
style: TextStyle(
|
||||
fontSize: 10 * scale,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,15 +10,13 @@ class MarkListScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MarkListScreenState extends State<MarkListScreen> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final provider = Provider.of<MarkListProvider>(context, listen: false);
|
||||
provider.init(context);
|
||||
provider.loadMarks(context); // Load full list again
|
||||
provider.loadMarks(context);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,27 +24,121 @@ class _MarkListScreenState extends State<MarkListScreen> {
|
||||
Widget build(BuildContext 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(
|
||||
backgroundColor: Colors.white,
|
||||
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
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: EdgeInsets.all(10 * scale),
|
||||
itemCount: marks.marks.length,
|
||||
itemBuilder: (_, i) {
|
||||
final m = marks.marks[i];
|
||||
final status =
|
||||
(m['status'] ?? '').toString().toLowerCase();
|
||||
|
||||
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),
|
||||
final LinearGradient statusGradient =
|
||||
status == 'active'
|
||||
? const LinearGradient(
|
||||
colors: [
|
||||
Color(0xFF2ECC71), // Green
|
||||
Color(0xFF1E8449), // Deep Emerald
|
||||
],
|
||||
)
|
||||
: const LinearGradient(
|
||||
colors: [
|
||||
Color(0xFFE74C3C), // Red
|
||||
Color(0xFFC0392B), // Dark Red
|
||||
],
|
||||
);
|
||||
|
||||
return Container(
|
||||
margin: EdgeInsets.only(bottom: 10 * scale),
|
||||
padding: EdgeInsets.all(12 * scale),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
Color(0xFF2196F3),
|
||||
Color(0xFF64B5F6),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(14 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.06),
|
||||
blurRadius: 5 * scale,
|
||||
offset: Offset(0, 2 * scale),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// LEFT TEXT
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
m['mark_no'],
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 3 * scale),
|
||||
Text(
|
||||
"${m['origin']} → ${m['destination']}",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13 * scale,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// STATUS BADGE (GREEN / RED)
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 10 * scale,
|
||||
vertical: 5 * scale,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
gradient: statusGradient,
|
||||
borderRadius: BorderRadius.circular(20 * scale),
|
||||
),
|
||||
child: Text(
|
||||
m['status'],
|
||||
style: TextStyle(
|
||||
fontSize: 11.5 * scale,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/dio_client.dart';
|
||||
import '../services/order_service.dart';
|
||||
|
||||
|
||||
class OrderDetailScreen extends StatefulWidget {
|
||||
final String orderId;
|
||||
const OrderDetailScreen({super.key, required this.orderId});
|
||||
@@ -14,6 +14,13 @@ class OrderDetailScreen extends StatefulWidget {
|
||||
class _OrderDetailScreenState extends State<OrderDetailScreen> {
|
||||
bool loading = true;
|
||||
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
|
||||
void initState() {
|
||||
@@ -33,105 +40,365 @@ class _OrderDetailScreenState extends State<OrderDetailScreen> {
|
||||
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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
String _initials(String? s) {
|
||||
if (s == null || s.isEmpty) return "I";
|
||||
final parts = s.split(" ");
|
||||
return parts.take(2).map((e) => e[0].toUpperCase()).join();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = order['items'] ?? [];
|
||||
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final scale = (width / 430).clamp(0.85, 1.20);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Order Details")),
|
||||
backgroundColor: const Color(0xFFF0F6FF),
|
||||
appBar: AppBar(
|
||||
title: const Text("Order Details"),
|
||||
elevation: 0,
|
||||
),
|
||||
body: loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ListView(
|
||||
children: [
|
||||
// ---------------- ORDER SUMMARY ----------------
|
||||
const Text(
|
||||
"Order Summary",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
padding: EdgeInsets.all(16 * scale),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
_summaryCard(scale),
|
||||
SizedBox(height: 18 * scale),
|
||||
_itemsSection(items, scale),
|
||||
SizedBox(height: 18 * scale),
|
||||
_totalsSection(scale),
|
||||
|
||||
_row("Order ID", order['order_id']),
|
||||
_row("Mark No", order['mark_no']),
|
||||
_row("Origin", order['origin']),
|
||||
_row("Destination", order['destination']),
|
||||
_row("Status", order['status']),
|
||||
SizedBox(height: 24 * scale),
|
||||
_confirmOrderButton(scale),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
const Text(
|
||||
"Totals",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
_row("CTN", order['ctn']),
|
||||
_row("Qty", order['qty']),
|
||||
_row("Total Qty", order['ttl_qty']),
|
||||
_row("Amount", "₹${order['ttl_amount'] ?? 0}"),
|
||||
_row("CBM", order['cbm']),
|
||||
_row("Total CBM", order['ttl_cbm']),
|
||||
_row("KG", order['kg']),
|
||||
_row("Total KG", order['ttl_kg']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
// ---------------- ORDER ITEMS ----------------
|
||||
const Text(
|
||||
"Order Items",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
...List.generate(items.length, (i) {
|
||||
final item = items[i];
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item['description'] ?? "No description",
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
|
||||
_row("Qty", item['qty']),
|
||||
_row("Unit", item['unit']),
|
||||
_row("CBM", item['cbm']),
|
||||
_row("KG", item['kg']),
|
||||
_row("Amount", "₹${item['ttl_amount'] ?? 0}"),
|
||||
_row("Shop No", item['shop_no']),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _confirmOrderButton(double scale) {
|
||||
final isPlaced = order['status'] == 'order_placed';
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52 * scale,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: isPlaced ? Colors.orange : Colors.green,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14 * scale),
|
||||
),
|
||||
),
|
||||
onPressed: (!isPlaced || confirming)
|
||||
? null
|
||||
: () async {
|
||||
setState(() => confirming = true);
|
||||
|
||||
final service =
|
||||
OrderService(DioClient.getInstance(context));
|
||||
|
||||
final res =
|
||||
await service.confirmOrder(order['order_id']);
|
||||
|
||||
confirming = false;
|
||||
|
||||
if (res['success'] == true) {
|
||||
setState(() {
|
||||
order['status'] = 'order_confirmed';
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Order confirmed successfully'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(res['message'] ?? 'Failed to confirm order'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
},
|
||||
child: confirming
|
||||
? const CircularProgressIndicator(color: Colors.white)
|
||||
: Text(
|
||||
isPlaced ? 'Confirm Order' : 'Order Confirmed',
|
||||
style: TextStyle(
|
||||
fontSize: 16 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------
|
||||
// SUMMARY CARD
|
||||
// -----------------------------
|
||||
Widget _summaryCard(double scale) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(18 * scale),
|
||||
decoration: _cardDecoration(scale),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Order Summary",
|
||||
style: TextStyle(fontSize: 20 * scale, fontWeight: FontWeight.bold)),
|
||||
SizedBox(height: 12 * scale),
|
||||
_infoRow("Order ID", order['order_id'], scale),
|
||||
_infoRow("Mark No", order['mark_no'], scale),
|
||||
_infoRow("Origin", order['origin'], scale),
|
||||
_infoRow("Destination", order['destination'], scale),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _infoRow(String title, dynamic value, double scale) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 6 * scale),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(title,
|
||||
style: TextStyle(color: Colors.grey, fontSize: 14 * scale)),
|
||||
Text(value?.toString() ?? "-",
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15 * scale)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// ORDER ITEMS SECTION
|
||||
// -----------------------------
|
||||
Widget _itemsSection(List items, double scale) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(18 * scale),
|
||||
decoration: _cardDecoration(scale),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Order Items",
|
||||
style: TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
|
||||
SizedBox(height: 16 * scale),
|
||||
...List.generate(items.length, (i) {
|
||||
return _expandableItem(items[i], i, scale);
|
||||
})
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// EXPANDABLE ITEM
|
||||
// -----------------------------
|
||||
Widget _expandableItem(Map item, int index, double scale) {
|
||||
final id = "item_$index";
|
||||
_expanded[id] = _expanded[id] ?? false;
|
||||
|
||||
final description = item['description'] ?? "Item";
|
||||
final initials = _initials(description);
|
||||
final imageUrl = item['image'] ?? "";
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
margin: EdgeInsets.only(bottom: 16 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14 * scale),
|
||||
border: Border.all(color: Colors.black12),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
minVerticalPadding: 10 * scale,
|
||||
leading: _avatar(imageUrl, initials, scale),
|
||||
title: Text(description,
|
||||
style:
|
||||
TextStyle(fontWeight: FontWeight.bold, fontSize: 15 * scale)),
|
||||
trailing: Transform.rotate(
|
||||
angle: (_expanded[id]! ? 3.14 : 0),
|
||||
child: Icon(Icons.keyboard_arrow_down, size: 24 * scale),
|
||||
),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_expanded[id] = !_expanded[id]!;
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
if (_expanded[id]!)
|
||||
Padding(
|
||||
padding: EdgeInsets.all(12 * scale),
|
||||
child: Column(
|
||||
children: [
|
||||
_pill("Qty", Icons.list_alt, "${item['qty']}",
|
||||
Colors.blue.shade100, scale),
|
||||
_pill("Unit", Icons.category, "${item['unit']}",
|
||||
Colors.orange.shade100, scale),
|
||||
_pill("KG", Icons.scale, "${item['kg']}",
|
||||
Colors.red.shade100, scale),
|
||||
_pill("CBM", Icons.straighten, "${item['cbm']}",
|
||||
Colors.purple.shade100, scale),
|
||||
_pill("Shop", Icons.storefront, "${item['shop_no']}",
|
||||
Colors.grey.shade300, scale),
|
||||
_pill("Amount", Icons.currency_rupee,
|
||||
"₹${item['ttl_amount']}",
|
||||
Colors.green.shade100, scale),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// AVATAR (RESPONSIVE)
|
||||
// -----------------------------
|
||||
Widget _avatar(String url, String initials, double scale) {
|
||||
return Container(
|
||||
width: 48 * scale,
|
||||
height: 48 * scale,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade200,
|
||||
borderRadius: BorderRadius.circular(10 * scale),
|
||||
),
|
||||
child: url.isNotEmpty
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10 * scale),
|
||||
child: Image.network(url, fit: BoxFit.cover,
|
||||
errorBuilder: (c, e, s) => Center(
|
||||
child: Text(initials,
|
||||
style: TextStyle(
|
||||
fontSize: 18 * scale,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold)),
|
||||
)),
|
||||
)
|
||||
: Center(
|
||||
child: Text(initials,
|
||||
style: TextStyle(
|
||||
fontSize: 18 * scale,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// COLOR-CODED PILL (RESPONSIVE)
|
||||
// -----------------------------
|
||||
Widget _pill(
|
||||
String title, IconData icon, String value, Color bgColor, double scale) {
|
||||
return Container(
|
||||
margin: EdgeInsets.only(bottom: 12 * scale),
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 14 * scale, vertical: 12 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(12 * scale),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 20 * scale, color: Colors.black54),
|
||||
SizedBox(width: 10 * scale),
|
||||
Text("$title: ",
|
||||
style:
|
||||
TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * scale)),
|
||||
Expanded(
|
||||
child: Text(value,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700, fontSize: 15 * scale)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// TOTAL SECTION
|
||||
// -----------------------------
|
||||
Widget _totalsSection(double scale) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(18 * scale),
|
||||
decoration: _cardDecoration(scale),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Totalss",
|
||||
style: TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
|
||||
SizedBox(height: 12 * scale),
|
||||
_totalRow("Total Qty", order['ttl_qty'], scale),
|
||||
_totalRow("Total KG", order['ttl_kg'], scale),
|
||||
SizedBox(height: 12 * scale),
|
||||
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(vertical: 20 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.shade50,
|
||||
borderRadius: BorderRadius.circular(12 * scale),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text("₹${order['ttl_amount'] ?? 0}",
|
||||
style: TextStyle(
|
||||
color: Colors.green,
|
||||
fontSize: 28 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
)),
|
||||
SizedBox(height: 4 * scale),
|
||||
Text("Total Amount",
|
||||
style: TextStyle(
|
||||
color: Colors.black54, fontSize: 14 * scale)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _totalRow(String title, dynamic value, double scale) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(title,
|
||||
style: TextStyle(color: Colors.grey, fontSize: 14 * scale)),
|
||||
Text(value?.toString() ?? "0",
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15 * scale)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// CARD DECORATION
|
||||
// -----------------------------
|
||||
BoxDecoration _cardDecoration(double scale) {
|
||||
return BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
blurRadius: 8 * scale,
|
||||
offset: Offset(0, 3 * scale)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||
import '../services/dio_client.dart';
|
||||
import '../services/order_service.dart';
|
||||
|
||||
|
||||
class OrderInvoiceScreen extends StatefulWidget {
|
||||
final String orderId;
|
||||
const OrderInvoiceScreen({super.key, required this.orderId});
|
||||
@@ -11,139 +10,442 @@ class OrderInvoiceScreen extends StatefulWidget {
|
||||
State<OrderInvoiceScreen> createState() => _OrderInvoiceScreenState();
|
||||
}
|
||||
|
||||
class _OrderInvoiceScreenState extends State<OrderInvoiceScreen> {
|
||||
class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
bool loading = true;
|
||||
bool controllerInitialized = false;
|
||||
|
||||
Map invoice = {};
|
||||
|
||||
late AnimationController _controller;
|
||||
late Animation<Offset> _slideAnimation;
|
||||
|
||||
bool s1 = true;
|
||||
bool s2 = false;
|
||||
bool s3 = false;
|
||||
bool s4 = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initializeController();
|
||||
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 {
|
||||
final service = OrderService(DioClient.getInstance(context));
|
||||
final res = await service.getInvoice(widget.orderId);
|
||||
|
||||
if (res['success'] == true) {
|
||||
invoice = res['invoice'] ?? {};
|
||||
if (res["success"] == true) {
|
||||
invoice = res["invoice"] ?? {};
|
||||
}
|
||||
|
||||
loading = false;
|
||||
setState(() {});
|
||||
if (mounted) setState(() => loading = false);
|
||||
}
|
||||
|
||||
Widget _row(String label, dynamic value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = invoice["items"] as List? ?? [];
|
||||
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final scale = (width / 430).clamp(0.85, 1.18);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Invoice"),
|
||||
),
|
||||
body: loading || !controllerInitialized
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: ListView(
|
||||
padding: EdgeInsets.all(16 * scale),
|
||||
children: [
|
||||
Text(label,
|
||||
style: const TextStyle(fontSize: 14, color: Colors.grey)),
|
||||
Text(value?.toString() ?? "N/A",
|
||||
style:
|
||||
const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)),
|
||||
_headerCard(scale),
|
||||
|
||||
_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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = invoice['items'] ?? [];
|
||||
// ---------------- HEADER CARD ----------------
|
||||
Widget _headerCard(double scale) {
|
||||
final statusColor = getInvoiceStatusColor(invoice["status"]);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Invoice")),
|
||||
body: loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ListView(
|
||||
children: [
|
||||
const Text("Invoice Summary",
|
||||
style:
|
||||
TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
_row("Invoice No", invoice['invoice_number']),
|
||||
_row("Invoice Date", invoice['invoice_date']),
|
||||
_row("Due Date", invoice['due_date']),
|
||||
_row("Payment Method", invoice['payment_method']),
|
||||
_row("Reference No", invoice['reference_no']),
|
||||
_row("Status", invoice['status']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
const Text("Amount Details",
|
||||
style:
|
||||
TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
_row("Amount (without GST)", invoice['final_amount']),
|
||||
_row("GST Amount", invoice['gst_amount']),
|
||||
_row("Final Amount With GST",
|
||||
invoice['final_amount_with_gst']),
|
||||
_row("Tax Type", invoice['tax_type']),
|
||||
_row("CGST %", invoice['cgst_percent']),
|
||||
_row("SGST %", invoice['sgst_percent']),
|
||||
_row("IGST %", invoice['igst_percent']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
_row("Customer Name", invoice['customer_name']),
|
||||
_row("Company Name", invoice['company_name']),
|
||||
_row("Email", invoice['customer_email']),
|
||||
_row("Mobile", invoice['customer_mobile']),
|
||||
_row("Address", invoice['customer_address']),
|
||||
_row("Pincode", invoice['pincode']),
|
||||
_row("Notes", invoice['notes']),
|
||||
|
||||
const Divider(height: 30),
|
||||
|
||||
// PDF DOWNLOAD
|
||||
if (invoice['pdf_path'] != null)
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
// open pdf
|
||||
},
|
||||
icon: const Icon(Icons.picture_as_pdf, color: Colors.red),
|
||||
label: const Text("Download PDF"),
|
||||
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,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const Text("Invoice Items",
|
||||
style:
|
||||
TextStyle(fontSize: 17, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
...List.generate(items.length, (i) {
|
||||
final item = items[i];
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: ListTile(
|
||||
title: Text(item['description'] ?? "Item"),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Qty: ${item['qty'] ?? 0}"),
|
||||
Text("Price: ₹${item['price'] ?? 0}"),
|
||||
],
|
||||
),
|
||||
trailing: Text(
|
||||
"₹${item['ttl_amount'] ?? 0}",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.indigo),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
// ---------------- SECTION HEADER ----------------
|
||||
Widget _sectionHeader(
|
||||
String title, IconData icon, bool expanded, Function toggle, double scale) {
|
||||
return GestureDetector(
|
||||
onTap: () => toggle(),
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(14 * scale),
|
||||
margin: EdgeInsets.only(bottom: 10 * scale),
|
||||
decoration: BoxDecoration(
|
||||
gradient:
|
||||
LinearGradient(colors: [Colors.blue.shade400, Colors.indigo.shade500]),
|
||||
borderRadius: BorderRadius.circular(14 * scale),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: Colors.white, size: 20 * scale),
|
||||
SizedBox(width: 10 * scale),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15 * scale,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
AnimatedRotation(
|
||||
turns: expanded ? .5 : 0,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: Icon(Icons.keyboard_arrow_down,
|
||||
color: Colors.white, size: 22 * scale),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------- SECTION BODY ----------------
|
||||
Widget _sectionBody(bool visible, List<Widget> children, double scale) {
|
||||
if (!controllerInitialized) return const SizedBox();
|
||||
|
||||
return AnimatedCrossFade(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
firstChild: const SizedBox.shrink(),
|
||||
secondChild: SlideTransition(
|
||||
position: _slideAnimation,
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(16 * scale),
|
||||
margin: EdgeInsets.only(bottom: 14 * scale),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(14 * scale),
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: 8 * scale,
|
||||
offset: Offset(0, 3 * scale),
|
||||
color: Colors.black.withOpacity(.08),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(children: children),
|
||||
),
|
||||
),
|
||||
crossFadeState:
|
||||
visible ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------- DETAIL ROW ----------------
|
||||
Widget _detailRow(IconData icon, String label, dynamic value, double scale) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 6 * scale),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: Colors.blueGrey, size: 20 * scale),
|
||||
SizedBox(width: 10 * scale),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(color: Colors.grey.shade700, fontSize: 14 * scale),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value?.toString() ?? "N/A",
|
||||
textAlign: TextAlign.end,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15 * scale,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------- ITEM TILE ----------------
|
||||
Widget _itemTile(Map item, double scale) {
|
||||
final qty = item['qty'] ?? 0;
|
||||
final price = item['price'] ?? 0;
|
||||
final total = item['ttl_amount'] ?? 0;
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.all(16 * scale),
|
||||
margin: EdgeInsets.only(bottom: 14 * scale),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16 * scale),
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: 8 * scale,
|
||||
offset: Offset(0, 3 * scale),
|
||||
color: Colors.black.withOpacity(.08),
|
||||
),
|
||||
],
|
||||
border: Border.all(color: Colors.grey.shade300, width: 1),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(8 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.indigo.shade50,
|
||||
borderRadius: BorderRadius.circular(10 * scale),
|
||||
),
|
||||
child: Icon(Icons.inventory_2,
|
||||
color: Colors.indigo, size: 20 * scale),
|
||||
),
|
||||
SizedBox(width: 12 * scale),
|
||||
Expanded(
|
||||
child: Text(
|
||||
item['description'] ?? "Item",
|
||||
style: TextStyle(
|
||||
fontSize: 16 * scale,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 14 * scale),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_itemBadge(Icons.numbers, "Qty", qty.toString(), false, scale),
|
||||
_itemBadge(Icons.currency_rupee, "Price", "₹$price", false, scale),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 12 * scale),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 10 * scale, horizontal: 14 * scale),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12 * scale),
|
||||
color: Colors.indigo.shade50,
|
||||
border: Border.all(color: Colors.indigo, width: 1.5 * scale),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.summarize,
|
||||
size: 18 * scale, color: Colors.indigo),
|
||||
SizedBox(width: 6 * scale),
|
||||
Text(
|
||||
"Total:",
|
||||
style: TextStyle(
|
||||
fontSize: 15 * scale,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.indigo,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
"₹$total",
|
||||
style: TextStyle(
|
||||
fontSize: 17 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.indigo,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------- BADGE ----------------
|
||||
Widget _itemBadge(
|
||||
IconData icon, String label, String value, bool highlight, double scale) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 8 * scale, horizontal: 12 * scale),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12 * scale),
|
||||
color: highlight ? Colors.indigo.shade50 : Colors.grey.shade100,
|
||||
border: Border.all(
|
||||
color: highlight ? Colors.indigo : Colors.grey.shade300,
|
||||
width: highlight ? 1.5 * scale : 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon,
|
||||
size: 16 * scale,
|
||||
color: highlight ? Colors.indigo : Colors.grey),
|
||||
SizedBox(width: 4 * scale),
|
||||
Text(
|
||||
"$label: ",
|
||||
style: TextStyle(
|
||||
fontSize: 13 * scale,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: highlight ? Colors.indigo : Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 14 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: highlight ? Colors.indigo : Colors.black,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- STATUS COLOR HELPER ----------------
|
||||
|
||||
Color getInvoiceStatusColor(String? status) {
|
||||
final s = (status ?? '')
|
||||
.toLowerCase()
|
||||
.replaceAll('_', ' ')
|
||||
.replaceAll('-', ' ')
|
||||
.trim();
|
||||
|
||||
if (s == 'paid') return Colors.green.shade600;
|
||||
if (s == 'pending') return Colors.orange.shade600;
|
||||
if (s == 'overdue') return Colors.red.shade600;
|
||||
if (s == 'cancelled' || s == 'canceled') return Colors.grey.shade600;
|
||||
if (s == 'in progress') return Colors.blue.shade600;
|
||||
if (s == 'draft') return Colors.purple.shade600;
|
||||
|
||||
return Colors.blueGrey;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/order_provider.dart';
|
||||
import 'order_detail_screen.dart';
|
||||
import 'order_shipment_screen.dart';
|
||||
import 'order_invoice_screen.dart';
|
||||
import 'order_track_screen.dart';
|
||||
|
||||
@@ -14,6 +13,7 @@ class OrdersScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _OrdersScreenState extends State<OrdersScreen> {
|
||||
String searchQuery = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -31,77 +31,359 @@ class _OrdersScreenState extends State<OrdersScreen> {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: provider.orders.length,
|
||||
itemBuilder: (_, i) {
|
||||
final o = provider.orders[i];
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final scale = (screenWidth / 420).clamp(0.85, 1.1);
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Order ID: ${o['order_id']}",
|
||||
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
Text(o['description']),
|
||||
Text("₹ ${o['amount']}"),
|
||||
Text(o['status'], style: const TextStyle(color: Colors.indigo)),
|
||||
final filteredOrders = provider.orders.where((o) {
|
||||
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();
|
||||
|
||||
const SizedBox(height: 10),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_btn("Order", () => _openOrderDetails(o['order_id'])),
|
||||
_btn("Shipment", () => _openShipment(o['order_id'])),
|
||||
_btn("Invoice", () => _openInvoice(o['order_id'])),
|
||||
_btn("Track", () => _openTrack(o['order_id'])),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
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 _btn(String text, VoidCallback onTap) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
color: Colors.indigo.shade50,
|
||||
),
|
||||
child: Text(text, style: const TextStyle(color: Colors.indigo)),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openOrderDetails(String id) {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (_) => OrderDetailScreen(orderId: id)));
|
||||
Widget _orderCard(Map<String, dynamic> o, double scale) {
|
||||
final progress = getProgress(o['status']);
|
||||
final percent = (progress * 100).toInt();
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
margin: EdgeInsets.only(bottom: 12 * scale),
|
||||
color: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(12 * scale), // 👈 tighter padding
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// HEADER
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Order #${o['order_id']}",
|
||||
style: TextStyle(
|
||||
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),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openShipment(String id) {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (_) => OrderShipmentScreen(orderId: id)));
|
||||
Widget _btn(
|
||||
IconData icon,
|
||||
String text,
|
||||
Color fg,
|
||||
Color bg,
|
||||
VoidCallback onTap,
|
||||
double scale,
|
||||
) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: 115 * scale, // 👈 makes button wider
|
||||
minHeight: 36 * scale, // 👈 makes button taller
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openInvoice(String id) {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (_) => OrderInvoiceScreen(orderId: id)));
|
||||
|
||||
// ================= STATUS BADGE =================
|
||||
|
||||
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 _openTrack(String id) {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (_) => OrderTrackScreen(orderId: id)));
|
||||
void _openOrderDetails(String id) =>
|
||||
Navigator.push(context,
|
||||
MaterialPageRoute(builder: (_) => OrderDetailScreen(orderId: id)));
|
||||
|
||||
void _openInvoice(String id) =>
|
||||
Navigator.push(context,
|
||||
MaterialPageRoute(builder: (_) => OrderInvoiceScreen(orderId: id)));
|
||||
|
||||
void _openTrack(String id) =>
|
||||
Navigator.push(context,
|
||||
MaterialPageRoute(builder: (_) => OrderTrackScreen(orderId: id)));
|
||||
}
|
||||
|
||||
// ================= PROGRESS BAR =================
|
||||
|
||||
class _AnimatedProgressBar extends StatelessWidget {
|
||||
final double progress;
|
||||
final double scale;
|
||||
|
||||
const _AnimatedProgressBar({required this.progress, required this.scale});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, c) {
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: 8, // 👈 thinner bar
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
height: 8,
|
||||
width: c.maxWidth * progress,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF4F8CFF), Color(0xFF8A4DFF)],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ================= STATUS LOGIC =================
|
||||
|
||||
double getProgress(String? status) {
|
||||
final s = (status ?? '').toLowerCase();
|
||||
if (s.contains("shipment ready")) return 0.05;
|
||||
if (s.contains("export")) return 0.10;
|
||||
if (s.contains("international")) return 0.20;
|
||||
if (s.contains("arrived")) return 0.30;
|
||||
if (s.contains("import")) return 0.40;
|
||||
if (s.contains("warehouse")) return 0.50;
|
||||
if (s.contains("domestic")) return 0.60;
|
||||
if (s.contains("out for")) return 0.90;
|
||||
if (s.contains("delivered")) return 1.0;
|
||||
return 0.05;
|
||||
}
|
||||
|
||||
_StatusConfig statusConfig(String? status) {
|
||||
final s = (status ?? '').toLowerCase();
|
||||
|
||||
if (s.contains("shipment ready")) {
|
||||
return _StatusConfig(Colors.blue.shade800, Colors.blue.shade50);
|
||||
}
|
||||
if (s.contains("export")) {
|
||||
return _StatusConfig(Colors.purple.shade800, Colors.purple.shade50);
|
||||
}
|
||||
if (s.contains("international")) {
|
||||
return _StatusConfig(Colors.red.shade800, Colors.red.shade50);
|
||||
}
|
||||
if (s.contains("arrived")) {
|
||||
return _StatusConfig(Colors.orange.shade800, Colors.orange.shade50);
|
||||
}
|
||||
if (s.contains("import")) {
|
||||
return _StatusConfig(Colors.teal.shade800, Colors.teal.shade50);
|
||||
}
|
||||
if (s.contains("warehouse")) {
|
||||
return _StatusConfig(Colors.brown.shade800, Colors.brown.shade50);
|
||||
}
|
||||
if (s.contains("domestic")) {
|
||||
return _StatusConfig(Colors.indigo.shade800, Colors.indigo.shade50);
|
||||
}
|
||||
if (s.contains("out for")) {
|
||||
return _StatusConfig(Colors.pink.shade800, Colors.pink.shade50);
|
||||
}
|
||||
if (s.contains("delivered")) {
|
||||
return _StatusConfig(Colors.green.shade800, Colors.green.shade50);
|
||||
}
|
||||
|
||||
return _StatusConfig(Colors.grey.shade800, Colors.grey.shade300);
|
||||
}
|
||||
|
||||
class _StatusConfig {
|
||||
final Color fg;
|
||||
final Color bg;
|
||||
_StatusConfig(this.fg, this.bg);
|
||||
}
|
||||
|
||||
// ================= STATUS LABEL MAP =================
|
||||
|
||||
const Map<String, String> statusLabelMap = {
|
||||
'shipment_ready': 'Shipment Ready',
|
||||
'export_custom': 'Export Custom',
|
||||
'international_transit': 'International Transit',
|
||||
'arrived_at_port': 'Arrived At Port',
|
||||
'import_custom': 'Import Custom',
|
||||
'warehouse': 'Warehouse Processing',
|
||||
'domestic_distribution': 'Domestic Distribution',
|
||||
'out_for_delivery': 'Out For Delivery',
|
||||
'delivered': 'Delivered',
|
||||
};
|
||||
|
||||
String formatStatusLabel(String? status) {
|
||||
if (status == null || status.isEmpty) return "Unknown";
|
||||
|
||||
return statusLabelMap[status.toLowerCase()] ??
|
||||
status.replaceAll('_', ' ').toUpperCase();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// lib/screens/order_track_screen.dart
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/dio_client.dart';
|
||||
import '../services/order_service.dart';
|
||||
|
||||
import 'order_screen.dart';
|
||||
|
||||
class OrderTrackScreen extends StatefulWidget {
|
||||
final String orderId;
|
||||
@@ -11,54 +13,675 @@ class OrderTrackScreen extends StatefulWidget {
|
||||
State<OrderTrackScreen> createState() => _OrderTrackScreenState();
|
||||
}
|
||||
|
||||
class _OrderTrackScreenState extends State<OrderTrackScreen> {
|
||||
class _OrderTrackScreenState extends State<OrderTrackScreen>
|
||||
with TickerProviderStateMixin {
|
||||
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
|
||||
void initState() {
|
||||
super.initState();
|
||||
load();
|
||||
}
|
||||
|
||||
Future<void> load() async {
|
||||
final service = OrderService(DioClient.getInstance(context));
|
||||
final res = await service.trackOrder(widget.orderId);
|
||||
progressController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 900),
|
||||
);
|
||||
|
||||
if (res['success'] == true) {
|
||||
data = res['track'];
|
||||
}
|
||||
shipController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1600),
|
||||
)..repeat(reverse: true);
|
||||
|
||||
loading = false;
|
||||
setState(() {});
|
||||
timelineController = AnimationController(
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final scale = (width / 430).clamp(0.75, 1.25);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Track Order")),
|
||||
appBar: AppBar(
|
||||
title: const Text("Shipment & Tracking"),
|
||||
elevation: 0.8,
|
||||
),
|
||||
body: loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(16 * scale),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Order ID: ${data['order_id']}"),
|
||||
Text("Shipment Status: ${data['shipment_status']}"),
|
||||
Text("Shipment Date: ${data['shipment_date']}"),
|
||||
_headerCard(scale),
|
||||
SizedBox(height: 16 * scale),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
Center(
|
||||
child: Icon(
|
||||
Icons.local_shipping,
|
||||
size: 100,
|
||||
color: Colors.indigo.shade300,
|
||||
),
|
||||
),
|
||||
_shipmentSummary(scale),
|
||||
SizedBox(height: 20 * scale),
|
||||
|
||||
_trackingStatus(scale),
|
||||
SizedBox(height: 16 * scale),
|
||||
|
||||
// Replace horizontal progress bar with vertical timeline
|
||||
_shipmentTimeline(scale),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- HEADER ----------------
|
||||
Widget _headerCard(double scale) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(14 * scale),
|
||||
decoration: _boxDecoration(scale),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(10 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(12 * scale),
|
||||
),
|
||||
child: Icon(Icons.local_shipping,
|
||||
color: Colors.blue, size: 28 * scale),
|
||||
),
|
||||
SizedBox(width: 12 * scale),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Order #${trackData['order_id'] ?? '-'}",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 16 * scale),
|
||||
),
|
||||
SizedBox(height: 6 * scale),
|
||||
Text(
|
||||
trackData["shipment_status"] ?? "-",
|
||||
style: TextStyle(color: Colors.black54, fontSize: 13 * scale),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------- SHIPMENT SUMMARY ----------------
|
||||
Widget _shipmentSummary(double scale) {
|
||||
if (shipment == null) {
|
||||
return _simpleCard(scale, "Shipment not created yet");
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.all(16 * scale),
|
||||
decoration: _boxDecoration(scale),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Shipment Summary",
|
||||
style:
|
||||
TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
|
||||
SizedBox(height: 12 * scale),
|
||||
|
||||
Container(
|
||||
padding: EdgeInsets.all(12 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(12 * scale),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.qr_code_2, color: Colors.blue, size: 22 * scale),
|
||||
SizedBox(width: 10 * scale),
|
||||
Text("Shipment ID: ${shipment!['shipment_id']}",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 15 * scale)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 14 * scale),
|
||||
_twoCol(
|
||||
"Status",
|
||||
formatStatusLabel(shipment!['status']),
|
||||
scale,
|
||||
),
|
||||
|
||||
_twoCol("Shipment Date", _fmt(shipment!['shipment_date']), scale),
|
||||
_twoCol("Origin", shipment!['origin'], scale),
|
||||
_twoCol("Destination", shipment!['destination'], scale),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _simpleCard(double scale, String text) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(14 * scale),
|
||||
decoration: _boxDecoration(scale),
|
||||
child: Text(text,
|
||||
style: TextStyle(fontSize: 15 * scale, color: Colors.grey.shade700)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _twoCol(String title, dynamic value, double scale) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 6 * scale),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(title,
|
||||
style: TextStyle(color: Colors.grey, fontSize: 13 * scale)),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value?.toString() ?? "-",
|
||||
textAlign: TextAlign.right,
|
||||
style:
|
||||
TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * scale),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------- TRACKING STATUS ----------------
|
||||
Widget _trackingStatus(double scale) {
|
||||
final delivered = _computeProgress() >= 1.0;
|
||||
|
||||
return Container(
|
||||
padding:
|
||||
EdgeInsets.symmetric(vertical: 10 * scale, horizontal: 14 * scale),
|
||||
decoration: BoxDecoration(
|
||||
gradient: delivered
|
||||
? const LinearGradient(colors: [Colors.green, Colors.lightGreen])
|
||||
: const LinearGradient(colors: [Colors.blue, Colors.purple]),
|
||||
borderRadius: BorderRadius.circular(40 * scale),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
delivered ? Icons.verified : Icons.local_shipping,
|
||||
color: Colors.white,
|
||||
size: 16 * scale,
|
||||
),
|
||||
SizedBox(width: 8 * scale),
|
||||
Text(
|
||||
formatStatusLabel(trackData["shipment_status"]),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13 * scale,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------- VERTICAL SHIPMENT TIMELINE ----------------
|
||||
Widget _shipmentTimeline(double scale) {
|
||||
final currentStepIndex = _getCurrentStepIndex();
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.all(16 * scale),
|
||||
decoration: _boxDecoration(scale),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.timeline, color: Colors.blue, size: 20 * scale),
|
||||
SizedBox(width: 8 * scale),
|
||||
Text(
|
||||
"Shipment Progress",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 17 * scale,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 8 * scale),
|
||||
Text(
|
||||
"Tracking your package in real-time",
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600,
|
||||
fontSize: 13 * scale,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16 * scale),
|
||||
|
||||
// Timeline Container
|
||||
AnimatedBuilder(
|
||||
animation: timelineController,
|
||||
builder: (context, child) {
|
||||
return Opacity(
|
||||
opacity: timelineController.value,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, 20 * (1 - timelineController.value)),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: _buildTimeline(scale, currentStepIndex),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTimeline(double scale, int currentStepIndex) {
|
||||
return Container(
|
||||
padding: EdgeInsets.only(left: 8 * scale),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: _shipmentSteps.length,
|
||||
itemBuilder: (context, index) {
|
||||
final step = _shipmentSteps[index];
|
||||
final isCompleted = index < currentStepIndex;
|
||||
final isCurrent = index == currentStepIndex;
|
||||
final isLast = index == _shipmentSteps.length - 1;
|
||||
|
||||
return _buildTimelineStep(
|
||||
scale: scale,
|
||||
step: step,
|
||||
index: index,
|
||||
isCompleted: isCompleted,
|
||||
isCurrent: isCurrent,
|
||||
isLast: isLast,
|
||||
currentStepIndex: currentStepIndex,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTimelineStep({
|
||||
required double scale,
|
||||
required Map<String, dynamic> step,
|
||||
required int index,
|
||||
required bool isCompleted,
|
||||
required bool isCurrent,
|
||||
required bool isLast,
|
||||
required int currentStepIndex,
|
||||
}) {
|
||||
return IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Timeline Column (Line + Icon)
|
||||
Column(
|
||||
children: [
|
||||
// Top connector line (except for first item)
|
||||
if (index > 0)
|
||||
Container(
|
||||
width: 2 * scale,
|
||||
height: 24 * scale,
|
||||
color: isCompleted ? Colors.green : Colors.grey.shade300,
|
||||
),
|
||||
|
||||
// Step Icon with animation
|
||||
_buildStepIcon(scale, isCompleted, isCurrent, step['icon']),
|
||||
|
||||
// Bottom connector line (except for last item)
|
||||
if (!isLast)
|
||||
Container(
|
||||
width: 2 * scale,
|
||||
height: 24 * scale,
|
||||
color: index < currentStepIndex ? Colors.green : Colors.grey.shade300,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(width: 16 * scale),
|
||||
|
||||
// Step Content
|
||||
Expanded(
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(bottom: 20 * scale),
|
||||
padding: EdgeInsets.all(14 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: isCurrent ? Colors.blue.shade50 : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12 * scale),
|
||||
border: isCurrent
|
||||
? Border.all(color: Colors.blue.shade200, width: 1.5 * scale)
|
||||
: null,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
step['title'],
|
||||
style: TextStyle(
|
||||
fontSize: 15 * scale,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isCurrent
|
||||
? Colors.blue.shade800
|
||||
: isCompleted
|
||||
? Colors.green.shade800
|
||||
: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isCompleted)
|
||||
Icon(Icons.check_circle,
|
||||
color: Colors.green,
|
||||
size: 18 * scale
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: 4 * scale),
|
||||
|
||||
// Optional timestamp - you can customize this with actual timestamps from API
|
||||
_buildStepTimestamp(scale, step, isCompleted, isCurrent, index),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStepIcon(double scale, bool isCompleted, bool isCurrent, IconData iconData) {
|
||||
// Animation for current step
|
||||
if (isCurrent) {
|
||||
return AnimatedBuilder(
|
||||
animation: shipController,
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
width: 32 * scale * (1 + 0.2 * shipController.value),
|
||||
height: 32 * scale * (1 + 0.2 * shipController.value),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.blue.shade50,
|
||||
border: Border.all(
|
||||
color: Colors.blue,
|
||||
width: 2 * scale,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
iconData,
|
||||
color: Colors.blue,
|
||||
size: 18 * scale,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Completed step
|
||||
if (isCompleted) {
|
||||
return Container(
|
||||
width: 32 * scale,
|
||||
height: 32 * scale,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.green.shade50,
|
||||
border: Border.all(
|
||||
color: Colors.green,
|
||||
width: 2 * scale,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
color: Colors.green,
|
||||
size: 18 * scale,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Upcoming step
|
||||
return Container(
|
||||
width: 32 * scale,
|
||||
height: 32 * scale,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.grey.shade100,
|
||||
border: Border.all(
|
||||
color: Colors.grey.shade400,
|
||||
width: 2 * scale,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
iconData,
|
||||
color: Colors.grey.shade600,
|
||||
size: 18 * scale,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStepTimestamp(double scale, Map<String, dynamic> step,
|
||||
bool isCompleted, bool isCurrent, int index) {
|
||||
// You can replace this with actual timestamps from your API
|
||||
final now = DateTime.now();
|
||||
final estimatedTime = now.add(Duration(days: index));
|
||||
|
||||
String statusText = isCurrent
|
||||
? "In progress"
|
||||
: isCompleted
|
||||
? "Completed"
|
||||
: "Estimated ${estimatedTime.day}/${estimatedTime.month}";
|
||||
|
||||
Color statusColor = isCurrent
|
||||
? Colors.blue.shade600
|
||||
: isCompleted
|
||||
? Colors.green.shade600
|
||||
: Colors.grey.shade600;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
isCurrent ? Icons.access_time : Icons.calendar_today,
|
||||
color: statusColor,
|
||||
size: 14 * scale,
|
||||
),
|
||||
SizedBox(width: 4 * scale),
|
||||
Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
fontSize: 12 * scale,
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
BoxDecoration _boxDecoration(double scale) {
|
||||
return BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12.withOpacity(0.05),
|
||||
blurRadius: 10 * scale,
|
||||
offset: Offset(0, 4 * scale),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,52 +16,124 @@ class _OtpScreenState extends State<OtpScreen> {
|
||||
final otpController = TextEditingController();
|
||||
bool verifying = false;
|
||||
|
||||
static const String defaultOtp = '123456'; // default OTP as you said
|
||||
static const String defaultOtp = '123456';
|
||||
|
||||
void _verifyAndSubmit() async {
|
||||
final entered = otpController.text.trim();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (entered != defaultOtp) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Invalid OTP')));
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(content: Text('Invalid OTP')));
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => verifying = true);
|
||||
|
||||
// send signup payload to backend
|
||||
final res = await RequestService(context).sendSignup(widget.signupPayload);
|
||||
|
||||
setState(() => verifying = false);
|
||||
|
||||
if (res['status'] == true || res['status'] == 'success') {
|
||||
// navigate to waiting screen
|
||||
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const WaitingScreen()));
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const WaitingScreen()));
|
||||
} else {
|
||||
final message = res['message']?.toString() ?? 'Failed';
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text(message)));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
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(
|
||||
appBar: AppBar(title: const Text("OTP Verification")),
|
||||
body: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: pad, vertical: 20),
|
||||
child: Column(children: [
|
||||
const Text("Enter the 6-digit OTP sent to your mobile/email. (Default OTP: 123456)"),
|
||||
const SizedBox(height: 20),
|
||||
RoundedInput(controller: otpController, hint: "Enter OTP", keyboardType: TextInputType.number),
|
||||
const SizedBox(height: 14),
|
||||
PrimaryButton(label: "Verify & Submit", onTap: _verifyAndSubmit, busy: verifying),
|
||||
]),
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
/// 🔙 Back Button
|
||||
Positioned(
|
||||
top: 18 * scale,
|
||||
left: 18 * scale,
|
||||
child: GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: Container(
|
||||
height: 42 * scale,
|
||||
width: 42 * scale,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.indigo,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.arrow_back,
|
||||
color: Colors.white, size: 22 * scale),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
/// 🟦 Center Card
|
||||
Center(
|
||||
child: Container(
|
||||
width: width * 0.90,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 20 * scale, vertical: 28 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 12 * scale,
|
||||
offset: Offset(0, 4 * scale),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"OTP Verification",
|
||||
style: TextStyle(
|
||||
fontSize: 22 * scale,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10 * scale),
|
||||
|
||||
Text(
|
||||
"Enter the 6-digit OTP sent to your mobile/email.\n(Default OTP: 123456)",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 13.5 * scale),
|
||||
),
|
||||
|
||||
SizedBox(height: 18 * scale),
|
||||
|
||||
RoundedInput(
|
||||
controller: otpController,
|
||||
hint: "Enter OTP",
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
|
||||
SizedBox(height: 22 * scale),
|
||||
|
||||
PrimaryButton(
|
||||
label: "Verify & Submit",
|
||||
onTap: _verifyAndSubmit,
|
||||
busy: verifying,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,13 +9,11 @@ import 'login_screen.dart';
|
||||
|
||||
class SettingsScreen extends StatefulWidget {
|
||||
const SettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SettingsScreen> createState() => _SettingsScreenState();
|
||||
}
|
||||
|
||||
class _SettingsScreenState extends State<SettingsScreen> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -27,32 +25,33 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
}
|
||||
|
||||
final profileProvider =
|
||||
Provider.of<UserProfileProvider>(context, listen: false);
|
||||
profileProvider.init(context);
|
||||
|
||||
await profileProvider.loadProfile(context);
|
||||
final profile = Provider.of<UserProfileProvider>(context, listen: false);
|
||||
profile.init(context);
|
||||
await profile.loadProfile(context);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _pickImage() async {
|
||||
final picked = await ImagePicker().pickImage(
|
||||
source: ImageSource.gallery,
|
||||
imageQuality: 80,
|
||||
);
|
||||
|
||||
if (picked != null) {
|
||||
final file = File(picked.path);
|
||||
final profileProvider =
|
||||
Provider.of<UserProfileProvider>(context, listen: false);
|
||||
|
||||
profileProvider.init(context);
|
||||
|
||||
final success = await profileProvider.updateProfileImage(context, file);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(success ? "Profile updated" : "Failed to update")),
|
||||
try {
|
||||
final picked = await ImagePicker().pickImage(
|
||||
source: ImageSource.gallery,
|
||||
imageQuality: 80,
|
||||
);
|
||||
if (picked != null) {
|
||||
final file = File(picked.path);
|
||||
final profile = Provider.of<UserProfileProvider>(context, listen: false);
|
||||
profile.init(context);
|
||||
final ok = await profile.updateProfileImage(context, file);
|
||||
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(ok ? "Profile updated" : "Failed to update")),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text("Error: $e")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,131 +61,309 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
title: const Text("Logout"),
|
||||
content: const Text("Are you sure you want to logout?"),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("Cancel")),
|
||||
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) {
|
||||
await auth.logout(context);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const LoginScreen()),
|
||||
(route) => false,
|
||||
(r) => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final profileProvider = Provider.of<UserProfileProvider>(context);
|
||||
|
||||
if (profileProvider.loading || profileProvider.profile == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final p = profileProvider.profile!;
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(18),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// ------------------ PROFILE IMAGE ------------------
|
||||
Center(
|
||||
child: GestureDetector(
|
||||
onTap: _pickImage,
|
||||
child: CircleAvatar(
|
||||
radius: 55,
|
||||
backgroundImage: p.profileImage != null
|
||||
? NetworkImage(p.profileImage!)
|
||||
: null,
|
||||
child: p.profileImage == null
|
||||
? const Icon(Icons.person, size: 55)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 25),
|
||||
|
||||
// ------------------ PROFILE INFO ------------------
|
||||
_infoRow("Customer ID", p.customerId),
|
||||
_infoRow("Name", p.customerName),
|
||||
_infoRow("Company", p.companyName),
|
||||
_infoRow("Email", p.email),
|
||||
_infoRow("Mobile", p.mobile),
|
||||
_infoRow("Address", p.address ?? "Not provided"),
|
||||
_infoRow("Pincode", p.pincode ?? "Not provided"),
|
||||
_infoRow("Status", p.status ?? "N/A"),
|
||||
_infoRow("Customer Type", p.customerType ?? "N/A"),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
Center(
|
||||
child: ElevatedButton(
|
||||
child: const Text("Edit Profile"),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const EditProfileScreen()),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// ------------------ LOGOUT BUTTON ------------------
|
||||
Center(
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 14),
|
||||
),
|
||||
icon: const Icon(Icons.logout, color: Colors.white),
|
||||
label: const Text(
|
||||
"Logout",
|
||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
onPressed: _logout,
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
|
||||
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
// ------------------------- REUSABLE FIELD ROW -------------------------
|
||||
Widget _fieldRow(IconData icon, String label, String value, double scale) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12 * scale),
|
||||
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
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)),
|
||||
]),
|
||||
)
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _infoRow(String title, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey)),
|
||||
const SizedBox(height: 4),
|
||||
Text(value, style: const TextStyle(fontSize: 16)),
|
||||
],
|
||||
// ------------------------- INFO TILE -------------------------
|
||||
Widget _infoTile(IconData icon, String title, String value, double scale) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(icon, size: 26 * scale, color: Colors.orange.shade800),
|
||||
SizedBox(width: 14 * scale),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title,
|
||||
style: TextStyle(
|
||||
fontSize: 13 * scale,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black54)),
|
||||
SizedBox(height: 4 * scale),
|
||||
Text(value,
|
||||
style: TextStyle(
|
||||
fontSize: 17 * scale, fontWeight: FontWeight.bold))
|
||||
]),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final profile = Provider.of<UserProfileProvider>(context);
|
||||
|
||||
if (profile.loading || profile.profile == null) {
|
||||
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||
}
|
||||
|
||||
// ----------- RESPONSIVE SCALE -----------
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final scale = (width / 390).clamp(0.80, 1.25);
|
||||
|
||||
final p = profile.profile!;
|
||||
final img = p.profileImage;
|
||||
final name = p.customerName ?? "Unknown";
|
||||
final email = p.email ?? "Not provided";
|
||||
final status = p.status ?? "Active";
|
||||
final cid = p.customerId ?? "—";
|
||||
final company = p.companyName ?? "—";
|
||||
final type = p.customerType ?? "—";
|
||||
final mobile = p.mobile ?? "—";
|
||||
final address = p.address ?? "Not provided";
|
||||
final pincode = p.pincode ?? "—";
|
||||
final isPartner = type.toLowerCase().contains("partner");
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFE9F2FF),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(18 * scale),
|
||||
child: Column(
|
||||
children: [
|
||||
// -------------------- PROFILE SECTION --------------------
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: _pickImage,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 64 * scale,
|
||||
backgroundColor: Colors.grey[200],
|
||||
backgroundImage:
|
||||
img != null ? NetworkImage(img) : null,
|
||||
child: img == null
|
||||
? Icon(Icons.person,
|
||||
size: 70 * scale, color: Colors.grey[600])
|
||||
: null,
|
||||
),
|
||||
|
||||
// ------------------ FIXED STATUS BADGE ------------------
|
||||
Positioned(
|
||||
bottom: 8 * scale,
|
||||
right: 8 * scale,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 12 * scale,
|
||||
vertical: 6 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: status.toLowerCase() == 'active'
|
||||
? Colors.green
|
||||
: Colors.orange,
|
||||
borderRadius: BorderRadius.circular(20 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 8 * scale)
|
||||
],
|
||||
),
|
||||
child: Text(
|
||||
status,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 14 * scale),
|
||||
Text(name,
|
||||
style: TextStyle(
|
||||
fontSize: 20 * scale,
|
||||
fontWeight: FontWeight.bold)),
|
||||
SizedBox(height: 6 * scale),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.email,
|
||||
size: 18 * scale, color: Colors.blueGrey),
|
||||
SizedBox(width: 8 * scale),
|
||||
Text(email,
|
||||
style: TextStyle(
|
||||
fontSize: 14 * scale,
|
||||
color: Colors.grey[700])),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 26 * scale),
|
||||
|
||||
// ---------------------- YELLOW SUMMARY CARD ----------------------
|
||||
Container(
|
||||
padding: EdgeInsets.all(18 * scale),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFFFFF8A3), Color(0xFFFFE275)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.orange.withOpacity(0.25),
|
||||
blurRadius: 14 * scale,
|
||||
offset: Offset(0, 8 * scale))
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_infoTile(Icons.badge, "Customer ID", cid, scale),
|
||||
Divider(height: 30 * scale),
|
||||
_infoTile(Icons.business, "Company Name", company, scale),
|
||||
Divider(height: 30 * scale),
|
||||
_infoTile(Icons.category, "Customer Type", type, scale),
|
||||
SizedBox(height: 20 * scale),
|
||||
|
||||
if (isPartner)
|
||||
Container(
|
||||
padding: EdgeInsets.all(14 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 10 * scale)
|
||||
],
|
||||
),
|
||||
child: Row(children: [
|
||||
Icon(Icons.workspace_premium,
|
||||
size: 32 * scale, color: Colors.amber[800]),
|
||||
SizedBox(width: 12 * scale),
|
||||
Text("Partner",
|
||||
style: TextStyle(
|
||||
fontSize: 16 * scale,
|
||||
fontWeight: FontWeight.bold)),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
),
|
||||
|
||||
SizedBox(height: 24 * scale),
|
||||
|
||||
// ---------------------- DETAILS CARD ----------------------
|
||||
Container(
|
||||
padding: EdgeInsets.all(16 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.06),
|
||||
blurRadius: 12 * scale)
|
||||
]),
|
||||
child: Column(
|
||||
children: [
|
||||
_fieldRow(Icons.phone_android, "Mobile", mobile, scale),
|
||||
const Divider(),
|
||||
_fieldRow(Icons.location_on, "Address", address, scale),
|
||||
const Divider(),
|
||||
_fieldRow(Icons.local_post_office, "Pincode", pincode, scale),
|
||||
SizedBox(height: 20 * scale),
|
||||
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) =>
|
||||
const EditProfileScreen()));
|
||||
},
|
||||
icon: Icon(Icons.edit,
|
||||
color: Colors.white, size: 18 * scale),
|
||||
label: Text("Edit Profile",
|
||||
style: TextStyle(
|
||||
color: Colors.white, fontSize: 14 * scale)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue[700],
|
||||
padding:
|
||||
EdgeInsets.symmetric(vertical: 14 * scale),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12 * scale)),
|
||||
),
|
||||
)),
|
||||
SizedBox(width: 12 * scale),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _logout,
|
||||
icon: Icon(Icons.logout,
|
||||
size: 18 * scale, color: Colors.white),
|
||||
label: Text("Logout",
|
||||
style: TextStyle(
|
||||
color: Colors.white, fontSize: 14 * scale)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red[600],
|
||||
padding:
|
||||
EdgeInsets.symmetric(vertical: 14 * scale),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12 * scale)),
|
||||
),
|
||||
)),
|
||||
])
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 30 * scale),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,16 +21,20 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
bool sending = false;
|
||||
|
||||
void _sendOtp() async {
|
||||
// We don't call backend for OTP here per your flow - OTP is default 123456.
|
||||
// Validate minimal
|
||||
if (cName.text.trim().isEmpty || cCompany.text.trim().isEmpty || cEmail.text.trim().isEmpty || cMobile.text.trim().isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please fill the required fields')));
|
||||
if (cName.text.trim().isEmpty ||
|
||||
cCompany.text.trim().isEmpty ||
|
||||
cEmail.text.trim().isEmpty ||
|
||||
cMobile.text.trim().isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Please fill the required fields')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => sending = true);
|
||||
await Future.delayed(const Duration(milliseconds: 600)); // UI feel
|
||||
await Future.delayed(const Duration(milliseconds: 600));
|
||||
setState(() => sending = false);
|
||||
// Navigate to OTP screen with collected data
|
||||
|
||||
final data = {
|
||||
'customer_name': cName.text.trim(),
|
||||
'company_name': cCompany.text.trim(),
|
||||
@@ -40,40 +44,152 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
'address': cAddress.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
|
||||
Widget build(BuildContext context) {
|
||||
final pad = MediaQuery.of(context).size.width * 0.06;
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Create Account")),
|
||||
backgroundColor: const Color(0xFFE8F0FF), // Same as Login background
|
||||
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: pad),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(children: [
|
||||
const SizedBox(height: 16),
|
||||
RoundedInput(controller: cName, hint: "Customer name"),
|
||||
const SizedBox(height: 12),
|
||||
RoundedInput(controller: cCompany, hint: "Company name"),
|
||||
const SizedBox(height: 12),
|
||||
RoundedInput(controller: cDesignation, hint: "Designation (optional)"),
|
||||
const SizedBox(height: 12),
|
||||
RoundedInput(controller: cEmail, hint: "Email", keyboardType: TextInputType.emailAddress),
|
||||
const SizedBox(height: 12),
|
||||
RoundedInput(controller: cMobile, hint: "Mobile", keyboardType: TextInputType.phone),
|
||||
const SizedBox(height: 12),
|
||||
RoundedInput(controller: cAddress, hint: "Address", maxLines: 3),
|
||||
const SizedBox(height: 12),
|
||||
RoundedInput(controller: cPincode, hint: "Pincode", keyboardType: TextInputType.number),
|
||||
const SizedBox(height: 20),
|
||||
PrimaryButton(label: "Send OTP", onTap: _sendOtp, busy: sending),
|
||||
const SizedBox(height: 14),
|
||||
]),
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16),
|
||||
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
/// 🔵 Back Button (scrolls with form)
|
||||
Material(
|
||||
elevation: 6,
|
||||
shape: const CircleBorder(),
|
||||
color: Colors.indigo.shade700,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Icon(Icons.arrow_back, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
/// 📦 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),
|
||||
|
||||
_blueInput(cCompany, "Company name"),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
_blueInput(cDesignation, "Designation (optional)"),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
_blueInput(
|
||||
cEmail,
|
||||
"Email",
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
_blueInput(
|
||||
cMobile,
|
||||
"Mobile",
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
_blueInput(
|
||||
cAddress,
|
||||
"Address",
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
_blueInput(
|
||||
cPincode,
|
||||
"Pincode",
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
|
||||
const SizedBox(height: 25),
|
||||
|
||||
PrimaryButton(
|
||||
label: "Send OTP",
|
||||
onTap: _sendOtp,
|
||||
busy: sending,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 🔵 Blue soft background input wrapper (same as Login)
|
||||
Widget _blueInput(
|
||||
TextEditingController controller,
|
||||
String hint, {
|
||||
TextInputType? keyboardType,
|
||||
int maxLines = 1,
|
||||
}) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFD8E7FF),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: RoundedInput(
|
||||
controller: controller,
|
||||
hint: hint,
|
||||
// keyboardType: keyboardType,
|
||||
maxLines: maxLines,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
import 'dashboard_screen.dart';
|
||||
import 'main_bottom_nav.dart';
|
||||
import 'welcome_screen.dart';
|
||||
|
||||
@@ -13,61 +12,166 @@ class SplashScreen extends StatefulWidget {
|
||||
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
|
||||
void 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();
|
||||
}
|
||||
|
||||
void _init() async {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
Future<void> _init() async {
|
||||
await Future.delayed(const Duration(milliseconds: 700));
|
||||
|
||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||
|
||||
// 🟢 IMPORTANT → WAIT FOR PREFERENCES TO LOAD
|
||||
await auth.init();
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (auth.isLoggedIn) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const MainBottomNav()),
|
||||
Future.delayed(const Duration(milliseconds: 900), () {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) =>
|
||||
auth.isLoggedIn ? const MainBottomNav() : const WelcomeScreen(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const WelcomeScreen()),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_mainController.dispose();
|
||||
_floatController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
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(
|
||||
body: Center(
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Container(
|
||||
width: size.width * 0.34,
|
||||
height: size.width * 0.34,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).primaryColor.withOpacity(0.14),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"K",
|
||||
style: TextStyle(
|
||||
fontSize: 48,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).primaryColor),
|
||||
),
|
||||
),
|
||||
body: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.blue.shade50,
|
||||
Colors.white,
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
const Text("Kent Logistics",
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)),
|
||||
]),
|
||||
),
|
||||
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(
|
||||
shape: BoxShape.circle,
|
||||
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: 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(
|
||||
fontSize: 22 * scale,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.1,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 6 * scale),
|
||||
|
||||
Text(
|
||||
"Delivering Excellence",
|
||||
style: TextStyle(
|
||||
fontSize: 14 * scale,
|
||||
color: Colors.black54,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,37 +1,198 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class WaitingScreen extends StatelessWidget {
|
||||
class WaitingScreen extends StatefulWidget {
|
||||
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
|
||||
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(
|
||||
appBar: AppBar(title: const Text("Request Submitted")),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(18.0),
|
||||
child: Center(
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(Icons.hourglass_top, size: 72, color: Theme.of(context).primaryColor),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"Signup request submitted successfully.",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
||||
textAlign: TextAlign.center,
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
/// BACK BUTTON
|
||||
Positioned(
|
||||
top: 12 * scale,
|
||||
left: 12 * scale,
|
||||
child: GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
"Please wait up to 24 hours for admin approval. You will receive an email once approved.",
|
||||
textAlign: TextAlign.center,
|
||||
|
||||
/// 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,
|
||||
),
|
||||
|
||||
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.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 14 * scale),
|
||||
),
|
||||
|
||||
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: () {
|
||||
Navigator.of(context)
|
||||
.popUntil((route) => route.isFirst);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
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(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,39 +1,188 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'signup_screen.dart';
|
||||
import 'login_screen.dart';
|
||||
import '../widgets/primary_button.dart';
|
||||
|
||||
class WelcomeScreen extends StatelessWidget {
|
||||
class WelcomeScreen extends StatefulWidget {
|
||||
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
|
||||
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(
|
||||
backgroundColor: const Color(0xFFE8F0FF),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: w * 0.06),
|
||||
padding: EdgeInsets.symmetric(horizontal: width * 0.07),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 28),
|
||||
Align(alignment: Alignment.centerLeft, child: Text("Welcome", style: Theme.of(context).textTheme.headlineSmall)),
|
||||
const SizedBox(height: 12),
|
||||
/// Animated Welcome text
|
||||
SlideTransition(
|
||||
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(
|
||||
"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))),
|
||||
|
||||
SizedBox(height: height * 0.04),
|
||||
|
||||
/// 🌈 Create Account Button (Gradient)
|
||||
PrimaryButton(
|
||||
label: "Create Account",
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const SignupScreen()),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton(
|
||||
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")))),
|
||||
style: OutlinedButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))),
|
||||
|
||||
SizedBox(height: height * 0.015),
|
||||
|
||||
/// 🌈 Login Button (Gradient)
|
||||
PrimaryButton(
|
||||
label: "Login",
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const LoginScreen()),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
SizedBox(height: height * 0.02),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -46,7 +46,7 @@ class AuthService {
|
||||
Future<Map<String, dynamic>> refreshToken(String oldToken) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
'/user/refresh',
|
||||
'/auth/refresh',
|
||||
options: Options(headers: {
|
||||
'Authorization': 'Bearer $oldToken',
|
||||
}),
|
||||
|
||||
62
lib/services/chat_realtime_service.dart
Normal file
62
lib/services/chat_realtime_service.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
import 'dart:convert';
|
||||
import 'package:pusher_channels_flutter/pusher_channels_flutter.dart';
|
||||
import '../services/dio_client.dart';
|
||||
|
||||
class ChatRealtimeService {
|
||||
static final PusherChannelsFlutter _pusher =
|
||||
PusherChannelsFlutter.getInstance();
|
||||
|
||||
static Future<void> connect({
|
||||
required int ticketId,
|
||||
required Function(Map<String, dynamic>) onMessage,
|
||||
}) async {
|
||||
|
||||
print(" 🧪🧪🧪🧪🧪🧪🧪🧪🧪SUBSCRIBING TO: private-ticket.$ticketId");
|
||||
|
||||
await _pusher.init(
|
||||
apiKey: "q5fkk5rvcnatvbgadwvl",
|
||||
cluster: "mt1",
|
||||
|
||||
onAuthorizer: (channelName, socketId, options) async {
|
||||
print("🧪🧪🧪🧪🧪🧪🧪🧪🧪🧪AUTHORIZING CHANNEL: $channelName");
|
||||
|
||||
final dio = DioClient.getInstance(options);
|
||||
|
||||
final res = await dio.post(
|
||||
'/broadcasting/auth',
|
||||
data: {
|
||||
'socket_id': socketId,
|
||||
'channel_name': channelName,
|
||||
},
|
||||
);
|
||||
|
||||
return res.data;
|
||||
},
|
||||
|
||||
onEvent: (event) {
|
||||
if (event.eventName == "NewChatMessage") {
|
||||
final data = jsonDecode(event.data);
|
||||
onMessage(Map<String, dynamic>.from(data));
|
||||
}
|
||||
},
|
||||
|
||||
onConnectionStateChange: (current, previous) {
|
||||
print("PUSHER STATE: $current");
|
||||
},
|
||||
|
||||
onError: (message, code, error) {
|
||||
print("PUSHER ERROR: $message");
|
||||
},
|
||||
);
|
||||
|
||||
await _pusher.subscribe(
|
||||
channelName: "private-ticket.$ticketId",
|
||||
);
|
||||
|
||||
await _pusher.connect();
|
||||
}
|
||||
|
||||
static Future<void> disconnect() async {
|
||||
await _pusher.disconnect();
|
||||
}
|
||||
}
|
||||
69
lib/services/chat_service.dart
Normal file
69
lib/services/chat_service.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import '../config/api_config.dart';
|
||||
|
||||
class ChatService {
|
||||
final Dio dio;
|
||||
|
||||
ChatService(this.dio);
|
||||
|
||||
/// Start chat / get ticket
|
||||
Future<Map<String, dynamic>> startChat() async {
|
||||
final res = await dio.get('/user/chat/start');
|
||||
return Map<String, dynamic>.from(res.data);
|
||||
}
|
||||
|
||||
/// Get all messages
|
||||
Future<List<dynamic>> getMessages(int ticketId) async {
|
||||
final res = await dio.get('/user/chat/messages/$ticketId');
|
||||
return res.data['messages'];
|
||||
}
|
||||
|
||||
/// Send message (text or file)
|
||||
Future<void> sendMessage(
|
||||
int ticketId, {
|
||||
String? message,
|
||||
String? clientId,
|
||||
}) async {
|
||||
final form = FormData();
|
||||
|
||||
if (message != null) form.fields.add(MapEntry('message', message));
|
||||
if (clientId != null) form.fields.add(MapEntry('client_id', clientId));
|
||||
|
||||
await dio.post('/user/chat/send/$ticketId', data: form);
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------
|
||||
// SEND FILE (image/video/pdf/excel)
|
||||
// ---------------------------
|
||||
Future<Map<String, dynamic>> sendFile(
|
||||
int ticketId,
|
||||
File file, {
|
||||
required Function(double) onProgress,
|
||||
}) async {
|
||||
final formData = FormData.fromMap({
|
||||
'file': await MultipartFile.fromFile(
|
||||
file.path,
|
||||
filename: file.path.split('/').last,
|
||||
),
|
||||
});
|
||||
|
||||
final res = await dio.post(
|
||||
"/user/chat/send/$ticketId",
|
||||
data: formData,
|
||||
options: Options(
|
||||
headers: {'Content-Type': 'multipart/form-data'},
|
||||
),
|
||||
onSendProgress: (sent, total) {
|
||||
if (total > 0) {
|
||||
onProgress(sent / total);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return Map<String, dynamic>.from(res.data['message']);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,7 +6,10 @@ import '../providers/auth_provider.dart';
|
||||
import 'token_interceptor.dart';
|
||||
|
||||
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) {
|
||||
if (_dio == null) {
|
||||
|
||||
@@ -29,4 +29,10 @@ class OrderService {
|
||||
final res = await _dio.get('/user/order/$id/track');
|
||||
return res.data;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> confirmOrder(String orderId) async {
|
||||
final res = await _dio.post('/user/orders/$orderId/confirm');
|
||||
return res.data;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
210
lib/services/reverb_socket_service.dart
Normal file
210
lib/services/reverb_socket_service.dart
Normal file
@@ -0,0 +1,210 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/dio_client.dart';
|
||||
|
||||
class ReverbSocketService {
|
||||
WebSocketChannel? _channel;
|
||||
Timer? _pingTimer;
|
||||
bool _connected = false;
|
||||
|
||||
late BuildContext _context;
|
||||
late int _ticketId;
|
||||
late Function(Map<String, dynamic>) _onMessage;
|
||||
late VoidCallback _onBackgroundMessage;
|
||||
late VoidCallback _onAdminMessage;
|
||||
|
||||
|
||||
/// Prevent duplicate messages on reconnect
|
||||
final Set<int> _receivedIds = {};
|
||||
|
||||
// ============================
|
||||
// CONNECT
|
||||
// ============================
|
||||
Future<void> connect({
|
||||
required BuildContext context,
|
||||
required int ticketId,
|
||||
required Function(Map<String, dynamic>) onMessage,
|
||||
//required VoidCallback onBackgroundMessage,
|
||||
required VoidCallback onAdminMessage, // 👈 ADD
|
||||
}) async {
|
||||
_context = context;
|
||||
_ticketId = ticketId;
|
||||
_onMessage = onMessage;
|
||||
//_onBackgroundMessage = onBackgroundMessage;
|
||||
_onAdminMessage = onAdminMessage; // 👈 SAVE
|
||||
|
||||
final uri = Uri.parse(
|
||||
'ws://10.119.0.74:8080/app/q5fkk5rvcnatvbgadwvl'
|
||||
'?protocol=7&client=flutter&version=1.0',
|
||||
);
|
||||
// final uri = Uri.parse(
|
||||
// 'ws://103.248.30.24:8080/app/q5fkk5rvcnatvbgadwvl'
|
||||
// '?protocol=7&client=flutter&version=1.0',
|
||||
// );
|
||||
|
||||
debugPrint("🔌 CONNECTING SOCKET → $uri");
|
||||
|
||||
_channel = WebSocketChannel.connect(uri);
|
||||
|
||||
_channel!.stream.listen(
|
||||
_handleMessage,
|
||||
onDone: _handleDisconnect,
|
||||
onError: (e) {
|
||||
debugPrint("❌ SOCKET ERROR: $e");
|
||||
_handleDisconnect();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ============================
|
||||
// HANDLE SOCKET MESSAGE
|
||||
// ============================
|
||||
Future<void> _handleMessage(dynamic raw) async {
|
||||
debugPrint("📥 RAW: $raw");
|
||||
|
||||
|
||||
final payload = jsonDecode(raw);
|
||||
final event = payload['event']?.toString() ?? '';
|
||||
|
||||
// --------------------------------
|
||||
// CONNECTED
|
||||
// --------------------------------
|
||||
if (event == 'pusher:connection_established') {
|
||||
final socketId = jsonDecode(payload['data'])['socket_id'];
|
||||
debugPrint("✅ CONNECTED | socket_id=$socketId");
|
||||
|
||||
await _subscribe(socketId);
|
||||
_startHeartbeat();
|
||||
_connected = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
// HEARTBEAT
|
||||
// --------------------------------
|
||||
if (event == 'pusher:pong') {
|
||||
debugPrint("💓 PONG");
|
||||
return;
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
// CHAT MESSAGE
|
||||
// --------------------------------
|
||||
if (
|
||||
event == 'NewChatMessage' ||
|
||||
event.endsWith('.NewChatMessage') ||
|
||||
event.contains('NewChatMessage')) {
|
||||
|
||||
dynamic data = payload['data'];
|
||||
if (data is String) data = jsonDecode(data);
|
||||
|
||||
final int msgId = data['id'];
|
||||
final String senderType = data['sender_type'] ?? '';
|
||||
final String? incomingClientId = data['client_id']; // ✅ HERE
|
||||
|
||||
// 🔁 Prevent duplicates by DB id
|
||||
if (_receivedIds.contains(msgId)) {
|
||||
debugPrint("🔁 DUPLICATE MESSAGE IGNORED: $msgId");
|
||||
return;
|
||||
}
|
||||
_receivedIds.add(msgId);
|
||||
|
||||
debugPrint("📩 NEW MESSAGE");
|
||||
debugPrint("🆔 id=$msgId");
|
||||
debugPrint("🧩 client_id=$incomingClientId");
|
||||
debugPrint("👤 sender=$senderType");
|
||||
debugPrint("💬 text=${data['message']}");
|
||||
|
||||
// ✅ Forward FULL payload (with client_id) to UI
|
||||
_onMessage(Map<String, dynamic>.from(data));
|
||||
|
||||
// 🔔 Unread count only for admin messages
|
||||
if (senderType == 'App\\Models\\Admin') {
|
||||
debugPrint("🔔 ADMIN MESSAGE → UNREAD +1");
|
||||
_onAdminMessage();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================
|
||||
// SUBSCRIBE PRIVATE CHANNEL
|
||||
// ============================
|
||||
Future<void> _subscribe(String socketId) async {
|
||||
debugPrint("📡 SUBSCRIBING private-ticket.$_ticketId");
|
||||
|
||||
final dio = DioClient.getInstance(_context);
|
||||
|
||||
final res = await dio.post(
|
||||
'/broadcasting/auth',
|
||||
data: {
|
||||
'socket_id': socketId,
|
||||
'channel_name': 'private-ticket.$_ticketId',
|
||||
},
|
||||
);
|
||||
|
||||
_channel!.sink.add(jsonEncode({
|
||||
'event': 'pusher:subscribe',
|
||||
'data': {
|
||||
'channel': 'private-ticket.$_ticketId',
|
||||
'auth': res.data['auth'],
|
||||
},
|
||||
}));
|
||||
|
||||
debugPrint("✅ SUBSCRIBED private-ticket.$_ticketId");
|
||||
}
|
||||
|
||||
// ============================
|
||||
// HEARTBEAT
|
||||
// ============================
|
||||
void _startHeartbeat() {
|
||||
_pingTimer?.cancel();
|
||||
|
||||
_pingTimer = Timer.periodic(
|
||||
const Duration(seconds: 30),
|
||||
(_) {
|
||||
if (_connected) {
|
||||
debugPrint("💓 SENDING PING");
|
||||
_channel?.sink.add(jsonEncode({
|
||||
'event': 'pusher:ping',
|
||||
'data': {}
|
||||
}));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ============================
|
||||
// HANDLE DISCONNECT
|
||||
// ============================
|
||||
void _handleDisconnect() {
|
||||
debugPrint("⚠️ SOCKET DISCONNECTED → RECONNECTING");
|
||||
|
||||
_connected = false;
|
||||
_pingTimer?.cancel();
|
||||
_channel?.sink.close();
|
||||
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
connect(
|
||||
context: _context,
|
||||
ticketId: _ticketId,
|
||||
onMessage: _onMessage,
|
||||
// onBackgroundMessage: _onBackgroundMessage,
|
||||
onAdminMessage: _onAdminMessage, // 👈 ADD
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================
|
||||
// MANUAL DISCONNECT
|
||||
// ============================
|
||||
void disconnect() {
|
||||
debugPrint("❌ SOCKET CLOSED MANUALLY");
|
||||
_connected = false;
|
||||
_pingTimer?.cancel();
|
||||
_channel?.sink.close();
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,130 @@
|
||||
import 'dart:async';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../config/api_config.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
|
||||
class TokenInterceptor extends Interceptor {
|
||||
final AuthProvider auth;
|
||||
final AuthProvider authProvider;
|
||||
final BuildContext context;
|
||||
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
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
if (auth.token != null) {
|
||||
options.headers['Authorization'] = 'Bearer ${auth.token}';
|
||||
void onRequest(
|
||||
RequestOptions options, RequestInterceptorHandler handler) async {
|
||||
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);
|
||||
}
|
||||
|
||||
// 🔄 Handle 401 & refresh token
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
||||
if (err.response?.statusCode == 401 &&
|
||||
err.response?.data['message'] == 'Token has expired') {
|
||||
debugPrint(
|
||||
'❌❌❌❌❌ [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) {
|
||||
err.requestOptions.headers['Authorization'] = 'Bearer ${auth.token}';
|
||||
final newResponse = await dio.fetch(err.requestOptions);
|
||||
return handler.resolve(newResponse);
|
||||
debugPrint('✅✅✅✅✅✅✅✅✅ [REFRESH] Refresh successful, retrying original 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);
|
||||
}
|
||||
|
||||
debugPrint('🚪🚪🚪🚪🚪🚪 [AUTH] Refresh failed → logging out user');
|
||||
await authProvider.logout(context);
|
||||
//await authProvider.forceLogout(context);
|
||||
|
||||
}
|
||||
|
||||
handler.next(err);
|
||||
}
|
||||
}
|
||||
|
||||
// 🔁 Call refresh API using SEPARATE Dio
|
||||
Future<bool> _refreshToken() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final oldToken = prefs.getString('token');
|
||||
|
||||
if (oldToken == null) {
|
||||
debugPrint('❌❌❌❌❌❌❌ [REFRESH] No old token found');
|
||||
return false;
|
||||
}
|
||||
|
||||
debugPrint('📤📤📤📤📤 [REFRESH] Calling /auth/refresh');
|
||||
|
||||
final refreshDio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: ApiConfig.baseUrl,
|
||||
headers: {
|
||||
'Authorization': 'Bearer $oldToken',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final res = await refreshDio.post('/auth/refresh');
|
||||
|
||||
if (res.data['success'] == true && res.data['token'] != null) {
|
||||
await prefs.setString('token', res.data['token']);
|
||||
debugPrint('💾💾💾💾💾💾💾 [REFRESH] New token saved to SharedPreferences');
|
||||
return true;
|
||||
}
|
||||
|
||||
debugPrint('❌❌❌❌❌❌ [REFRESH] API responded but refresh not allowed');
|
||||
return false;
|
||||
} catch (e) {
|
||||
debugPrint('🔥🔥🔥🔥🔥🔥 [REFRESH] Exception occurred: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
118
lib/widgets/chat_file_actions.dart
Normal file
118
lib/widgets/chat_file_actions.dart
Normal file
@@ -0,0 +1,118 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import '../screens/chat_file_viewer.dart';
|
||||
|
||||
class ChatFileActions {
|
||||
static void show(
|
||||
BuildContext context, {
|
||||
required String url,
|
||||
required String fileType,
|
||||
}) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (_) {
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_actionTile(
|
||||
icon: Icons.open_in_new,
|
||||
title: "Open",
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
ChatFileViewer.open(
|
||||
context,
|
||||
url: url,
|
||||
fileType: fileType,
|
||||
);
|
||||
},
|
||||
),
|
||||
_actionTile(
|
||||
icon: Icons.download,
|
||||
title: "Download",
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
await _downloadFile(context, url, fileType);
|
||||
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _actionTile({
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return ListTile(
|
||||
leading: Icon(icon),
|
||||
title: Text(title),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
static Future<void> _scanFile(String path) async {
|
||||
const platform = MethodChannel('media_scanner');
|
||||
|
||||
try {
|
||||
await platform.invokeMethod('scanFile', {'path': path});
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
|
||||
// ===========================
|
||||
// DOWNLOAD LOGIC
|
||||
// ===========================
|
||||
static Future<void> _downloadFile(
|
||||
BuildContext context,
|
||||
String url,
|
||||
String fileType,
|
||||
) async {
|
||||
try {
|
||||
final fileName = url.split('/').last;
|
||||
|
||||
Directory baseDir;
|
||||
|
||||
if (fileType.startsWith('image/')) {
|
||||
baseDir = Directory('/storage/emulated/0/Pictures/KentChat');
|
||||
} else if (fileType.startsWith('video/')) {
|
||||
baseDir = Directory('/storage/emulated/0/Movies/KentChat');
|
||||
} else {
|
||||
baseDir = Directory('/storage/emulated/0/Download/KentChat');
|
||||
}
|
||||
|
||||
if (!await baseDir.exists()) {
|
||||
await baseDir.create(recursive: true);
|
||||
}
|
||||
|
||||
final filePath = "${baseDir.path}/$fileName";
|
||||
|
||||
await Dio().download(url, filePath);
|
||||
|
||||
// 🔔 IMPORTANT: Tell Android to scan the file
|
||||
await _scanFile(filePath);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Saved to ${baseDir.path}")),
|
||||
);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("Download failed")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
127
lib/widgets/chat_file_preview.dart
Normal file
127
lib/widgets/chat_file_preview.dart
Normal file
@@ -0,0 +1,127 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/dio_client.dart';
|
||||
import '../screens/chat_file_viewer.dart';
|
||||
import '../widgets/chat_file_actions.dart';
|
||||
|
||||
class ChatFilePreview extends StatelessWidget {
|
||||
final String filePath;
|
||||
final String fileType;
|
||||
final bool isUser;
|
||||
|
||||
final bool isLocal;
|
||||
|
||||
const ChatFilePreview({
|
||||
super.key,
|
||||
required this.filePath,
|
||||
required this.fileType,
|
||||
required this.isUser,
|
||||
this.isLocal = false,
|
||||
});
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final url = "${DioClient.baseUrl}/storage/$filePath";
|
||||
final textColor = isUser ? Colors.white : Colors.black;
|
||||
|
||||
// LOCAL IMAGE (uploading preview)
|
||||
if (isLocal && fileType.startsWith('image/')) {
|
||||
return Image.file(
|
||||
File(filePath),
|
||||
width: 180,
|
||||
height: 120,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
// ✅ TAP = OPEN
|
||||
ChatFileViewer.open(
|
||||
context,
|
||||
url: url,
|
||||
fileType: fileType,
|
||||
);
|
||||
},
|
||||
onLongPress: () {
|
||||
// ✅ LONG PRESS = OPEN / DOWNLOAD
|
||||
ChatFileActions.show(
|
||||
context,
|
||||
url: url,
|
||||
fileType: fileType,
|
||||
);
|
||||
},
|
||||
child: _buildPreviewUI(textColor, url),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPreviewUI(Color textColor, String url) {
|
||||
// IMAGE
|
||||
if (fileType.startsWith('image/')) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
url,
|
||||
width: 180,
|
||||
height: 120,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// VIDEO
|
||||
if (fileType.startsWith('video/')) {
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 180,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black26,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.videocam,
|
||||
size: 50,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.play_circle_fill,
|
||||
size: 56,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// PDF / OTHER FILES
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(_fileIcon(fileType), color: textColor),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
_fileLabel(fileType),
|
||||
style: TextStyle(color: textColor),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
IconData _fileIcon(String type) {
|
||||
if (type == 'application/pdf') return Icons.picture_as_pdf;
|
||||
if (type.contains('excel')) return Icons.table_chart;
|
||||
return Icons.insert_drive_file;
|
||||
}
|
||||
|
||||
String _fileLabel(String type) {
|
||||
if (type == 'application/pdf') return "PDF document";
|
||||
if (type.contains('excel')) return "Excel file";
|
||||
return "Download file";
|
||||
}
|
||||
}
|
||||
356
lib/widgets/invoice_detail_view.dart
Normal file
356
lib/widgets/invoice_detail_view.dart
Normal file
@@ -0,0 +1,356 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class InvoiceDetailView extends StatefulWidget {
|
||||
final Map invoice;
|
||||
const InvoiceDetailView({super.key, required this.invoice});
|
||||
|
||||
@override
|
||||
State<InvoiceDetailView> createState() => _InvoiceDetailViewState();
|
||||
}
|
||||
|
||||
class _InvoiceDetailViewState extends State<InvoiceDetailView>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<Offset> _slideAnimation;
|
||||
|
||||
bool s1 = true;
|
||||
bool s2 = false;
|
||||
bool s3 = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 350),
|
||||
);
|
||||
|
||||
_slideAnimation = Tween(
|
||||
begin: const Offset(0, -0.1),
|
||||
end: Offset.zero,
|
||||
).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
|
||||
);
|
||||
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// STATUS BADGE (WHITE + GRADIENT BORDER)
|
||||
// ------------------------------------------------------------------
|
||||
Widget statusBadge(String status, double scale) {
|
||||
final s = status.toLowerCase();
|
||||
|
||||
LinearGradient gradient;
|
||||
Color textColor;
|
||||
|
||||
switch (s) {
|
||||
case 'paid':
|
||||
gradient = const LinearGradient(
|
||||
colors: [Color(0xFF2ECC71), Color(0xFF27AE60)],
|
||||
);
|
||||
textColor = const Color(0xFF27AE60);
|
||||
break;
|
||||
|
||||
case 'pending':
|
||||
gradient = const LinearGradient(
|
||||
colors: [
|
||||
Color(0xFF5B8DEF), // Soft Blue
|
||||
Color(0xFF7B5CFA), // Purple Blue
|
||||
],
|
||||
);
|
||||
textColor = const Color(0xFF5B8DEF);
|
||||
break;
|
||||
|
||||
case 'overdue':
|
||||
gradient = const LinearGradient(
|
||||
colors: [
|
||||
Color(0xFFFFB300), // Amber
|
||||
Color(0xFFFF6F00), // Deep Orange
|
||||
],
|
||||
);
|
||||
textColor = const Color(0xFFFF6F00);
|
||||
break;
|
||||
|
||||
case 'cancelled':
|
||||
case 'failed':
|
||||
gradient = const LinearGradient(
|
||||
colors: [Color(0xFFE74C3C), Color(0xFFC0392B)],
|
||||
);
|
||||
textColor = const Color(0xFFE74C3C);
|
||||
break;
|
||||
|
||||
default:
|
||||
gradient = const LinearGradient(
|
||||
colors: [Color(0xFF95A5A6), Color(0xFF7F8C8D)],
|
||||
);
|
||||
textColor = Colors.grey.shade700;
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(1.5),
|
||||
decoration: BoxDecoration(
|
||||
gradient: gradient,
|
||||
borderRadius: BorderRadius.circular(20 * scale),
|
||||
),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 14 * scale,
|
||||
vertical: 6 * scale,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(18 * scale),
|
||||
),
|
||||
child: Text(
|
||||
status.toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 12 * scale,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: textColor,
|
||||
letterSpacing: .6,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// HEADER CARD
|
||||
// ------------------------------------------------------------------
|
||||
Widget headerCard(Map invoice, double scale) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(16 * scale),
|
||||
margin: EdgeInsets.only(bottom: 14 * scale),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.blue.shade400, Colors.indigo.shade600],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: 8 * scale,
|
||||
offset: Offset(0, 3 * scale),
|
||||
color: Colors.indigo.withOpacity(.3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Invoice #${invoice['invoice_number']}",
|
||||
style: TextStyle(
|
||||
fontSize: 20 * scale,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4 * scale),
|
||||
Text(
|
||||
"Date: ${invoice['invoice_date']}",
|
||||
style: TextStyle(color: Colors.white70, fontSize: 13 * scale),
|
||||
),
|
||||
SizedBox(height: 10 * scale),
|
||||
|
||||
/// STATUS BADGE
|
||||
statusBadge(invoice['status'] ?? 'Unknown', scale),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// SECTION HEADER
|
||||
// ------------------------------------------------------------------
|
||||
Widget sectionHeader(
|
||||
String title,
|
||||
IconData icon,
|
||||
bool expanded,
|
||||
VoidCallback tap,
|
||||
double scale,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: tap,
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(12 * scale),
|
||||
margin: EdgeInsets.only(bottom: 8 * scale),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.indigo.shade500, Colors.blue.shade400],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: 5 * scale,
|
||||
color: Colors.black.withOpacity(.15),
|
||||
offset: Offset(0, 2 * scale),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: Colors.white, size: 20 * scale),
|
||||
SizedBox(width: 8 * scale),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 15 * scale,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
AnimatedRotation(
|
||||
turns: expanded ? .5 : 0,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: Colors.white,
|
||||
size: 20 * scale,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// SECTION BODY
|
||||
// ------------------------------------------------------------------
|
||||
Widget sectionBody(bool visible, List<Widget> children, double scale) {
|
||||
return AnimatedCrossFade(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
crossFadeState:
|
||||
visible ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
||||
firstChild: const SizedBox.shrink(),
|
||||
secondChild: SlideTransition(
|
||||
position: _slideAnimation,
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(14 * scale),
|
||||
margin: EdgeInsets.only(bottom: 10 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(12 * scale),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: 7 * scale,
|
||||
offset: Offset(0, 2 * scale),
|
||||
color: Colors.black.withOpacity(.07),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Column(children: children),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// DETAIL ROW
|
||||
// ------------------------------------------------------------------
|
||||
Widget detailRow(IconData icon, String label, dynamic value, double scale) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 5 * scale),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 18 * scale, color: Colors.blueGrey),
|
||||
SizedBox(width: 8 * scale),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600,
|
||||
fontSize: 13 * scale,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: label == "Status"
|
||||
? Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: statusBadge(value ?? 'Unknown', scale),
|
||||
)
|
||||
: Text(
|
||||
value?.toString() ?? "N/A",
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(
|
||||
fontSize: 14 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// MAIN BUILD
|
||||
// ------------------------------------------------------------------
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final invoice = widget.invoice;
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final scale = (width / 390).clamp(0.85, 1.25);
|
||||
|
||||
return ListView(
|
||||
padding: EdgeInsets.all(14 * scale),
|
||||
children: [
|
||||
headerCard(invoice, scale),
|
||||
|
||||
sectionHeader(
|
||||
"Invoice Summary",
|
||||
Icons.receipt_long,
|
||||
s1,
|
||||
() => setState(() => s1 = !s1),
|
||||
scale,
|
||||
),
|
||||
sectionBody(s1, [
|
||||
detailRow(Icons.numbers, "Invoice No", invoice['invoice_number'], scale),
|
||||
detailRow(Icons.calendar_today, "Date", invoice['invoice_date'], scale),
|
||||
detailRow(Icons.label, "Status", invoice['status'], scale),
|
||||
], scale),
|
||||
|
||||
sectionHeader(
|
||||
"Customer Details",
|
||||
Icons.person,
|
||||
s2,
|
||||
() => setState(() => s2 = !s2),
|
||||
scale,
|
||||
),
|
||||
sectionBody(s2, [
|
||||
detailRow(Icons.person_outline, "Name", invoice['customer_name'], scale),
|
||||
detailRow(Icons.mail, "Email", invoice['customer_email'], scale),
|
||||
detailRow(Icons.phone, "Phone", invoice['customer_mobile'], scale),
|
||||
detailRow(Icons.location_on, "Address", invoice['customer_address'], scale),
|
||||
], scale),
|
||||
|
||||
sectionHeader(
|
||||
"Amount Details",
|
||||
Icons.currency_rupee,
|
||||
s3,
|
||||
() => setState(() => s3 = !s3),
|
||||
scale,
|
||||
),
|
||||
sectionBody(s3, [
|
||||
detailRow(Icons.money, "Amount", invoice['final_amount'], scale),
|
||||
detailRow(Icons.percent, "GST %", invoice['gst_percent'], scale),
|
||||
detailRow(Icons.percent, "GST Amount", invoice['gst_amount'], scale),
|
||||
detailRow(Icons.summarize, "Total", invoice['final_amount_with_gst'], scale),
|
||||
], scale),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ class MainAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final profileUrl = profileProvider.profile?.profileImage;
|
||||
|
||||
return AppBar(
|
||||
backgroundColor: Colors.lightGreen,
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0.8,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
automaticallyImplyLeading: false,
|
||||
|
||||
@@ -1,19 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PrimaryButton extends StatelessWidget {
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: busy ? null : onTap,
|
||||
style: ElevatedButton.styleFrom(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)),
|
||||
|
||||
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(
|
||||
onPressed: busy ? null : onTap,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors
|
||||
.transparent, // IMPORTANT: keep transparent to see gradient
|
||||
shadowColor: Colors.transparent,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
|
||||
child: busy
|
||||
? const SizedBox(
|
||||
height: 18,
|
||||
width: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 16, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,16 +19,27 @@ class RoundedInput extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final radius = BorderRadius.circular(12);
|
||||
|
||||
return TextField(
|
||||
controller: controller,
|
||||
keyboardType: keyboardType,
|
||||
obscureText: obscure,
|
||||
maxLines: maxLines,
|
||||
maxLines: obscure ? 1 : maxLines,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey, // grey input text
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: const Color(0xFFD8E7FF), // light blue background
|
||||
hintText: hint,
|
||||
hintStyle: const TextStyle(
|
||||
color: Colors.grey, // grey hint text
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
border: OutlineInputBorder(borderRadius: radius, borderSide: BorderSide.none),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: radius,
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,9 +7,13 @@
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_linux
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
@@ -6,11 +6,21 @@ import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import file_selector_macos
|
||||
import package_info_plus
|
||||
import path_provider_foundation
|
||||
import share_plus
|
||||
import shared_preferences_foundation
|
||||
import url_launcher_macos
|
||||
import video_player_avfoundation
|
||||
import wakelock_plus
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
|
||||
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||
}
|
||||
|
||||
418
pubspec.lock
418
pubspec.lock
@@ -1,6 +1,22 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -9,6 +25,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -25,6 +57,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
chewie:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: chewie
|
||||
sha256: "44bcfc5f0dfd1de290c87c9d86a61308b3282a70b63435d5557cfd60f54a69ca"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.13.0"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -57,6 +97,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -65,6 +113,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dbus
|
||||
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
dio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -105,6 +161,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -129,6 +209,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -137,6 +225,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+5"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -150,6 +246,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -176,6 +280,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.5"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: html
|
||||
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.6"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -192,6 +304,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.4"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -256,6 +376,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -328,6 +456,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -336,8 +480,16 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
path_parsing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_parsing
|
||||
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
@@ -384,6 +536,78 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -400,6 +624,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
posix:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: posix
|
||||
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.3"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -408,6 +640,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -525,6 +789,78 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -533,6 +869,46 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -541,6 +917,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -549,6 +941,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -557,6 +965,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.1"
|
||||
sdks:
|
||||
dart: ">=3.8.1 <4.0.0"
|
||||
flutter: ">=3.32.0"
|
||||
|
||||
16
pubspec.yaml
16
pubspec.yaml
@@ -37,6 +37,20 @@ dependencies:
|
||||
google_fonts: ^4.0.3
|
||||
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.
|
||||
@@ -64,6 +78,8 @@ flutter:
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
assets:
|
||||
- assets/Images/K.png
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
|
||||
@@ -7,8 +7,17 @@
|
||||
#include "generated_plugin_registrant.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) {
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_windows
|
||||
permission_handler_windows
|
||||
share_plus
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
Reference in New Issue
Block a user