Compare commits

10 Commits
Master ... main

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

File diff suppressed because one or more lines are too long

3
.fvmrc Normal file
View File

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

3
.gitignore vendored
View File

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

View File

@@ -4,11 +4,20 @@
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.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"

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -1,3 +1,8 @@
// class ApiConfig {
// static const String baseUrl = "http://103.248.30.24:3030/api";
// static const String fileBaseUrl = "http://103.248.30.24:3030/";
// }
class ApiConfig {
static const String baseUrl = "http://10.207.50.74:8000/api";
static const String baseUrl = "http://10.119.0.74:8000/api";
static const String fileBaseUrl = "http://10.119.0.74:8000/";
}

View File

@@ -6,7 +6,7 @@ class AppConfig {
static const String logoUrlEmulator = "http://10.0.2.2:8000/images/kent_logo2.png";
// 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

View File

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

View File

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

View File

@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
class ChatUnreadProvider extends ChangeNotifier {
int _unreadCount = 0;
bool _chatOpen = false;
int get unreadCount => _unreadCount;
bool get isChatOpen => _chatOpen;
/// 📩 Called when ADMIN sends message
void increment() {
if (!_chatOpen) {
_unreadCount++;
notifyListeners();
}
}
/// 👁 Called when chat screen is opened
void setChatOpen(bool open) {
_chatOpen = open;
if (open) {
_unreadCount = 0; // reset badge when user opens chat
}
notifyListeners();
}
/// 🔁 Manual reset (optional)
void reset() {
_unreadCount = 0;
notifyListeners();
}
}

View File

@@ -4,7 +4,11 @@ import '../services/invoice_service.dart';
class InvoiceInstallmentScreen extends StatefulWidget {
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,
),
),
),
],
);
}
}

View File

@@ -0,0 +1,145 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
import 'package:video_player/video_player.dart';
import 'package:chewie/chewie.dart';
import 'package:flutter_pdfview/flutter_pdfview.dart';
import 'package:path_provider/path_provider.dart';
import 'package:dio/dio.dart';
class ChatFileViewer {
/// Entry point used by chat screen
static void open(
BuildContext context, {
required String url,
required String fileType,
}) {
if (fileType.startsWith('image/')) {
_openImage(context, url);
} else if (fileType.startsWith('video/')) {
_openVideo(context, url);
} else if (fileType == 'application/pdf') {
_openPdf(context, url);
} else {
_downloadFile(context, url);
}
}
// ===========================
// IMAGE VIEWER
// ===========================
static void _openImage(BuildContext context, String url) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(backgroundColor: Colors.black),
body: PhotoView(
imageProvider: NetworkImage(url),
),
),
),
);
}
// ===========================
// VIDEO VIEWER
// ===========================
static void _openVideo(BuildContext context, String url) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => _VideoPlayerPage(url: url),
),
);
}
// ===========================
// PDF VIEWER
// ===========================
static Future<void> _openPdf(
BuildContext context, String url) async {
final dir = await getTemporaryDirectory();
final path = "${dir.path}/chat.pdf";
await Dio().download(url, path);
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => Scaffold(
appBar: AppBar(title: const Text("PDF")),
body: PDFView(filePath: path),
),
),
);
}
// ===========================
// FILE DOWNLOAD
// ===========================
static Future<void> _downloadFile(
BuildContext context, String url) async {
final dir = await getExternalStorageDirectory();
final fileName = url.split('/').last;
final path = "${dir!.path}/$fileName";
await Dio().download(url, path);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("File downloaded: $fileName")),
);
}
}
// ===========================
// VIDEO PLAYER WIDGET
// ===========================
class _VideoPlayerPage extends StatefulWidget {
final String url;
const _VideoPlayerPage({required this.url});
@override
State<_VideoPlayerPage> createState() => _VideoPlayerPageState();
}
class _VideoPlayerPageState extends State<_VideoPlayerPage> {
late VideoPlayerController _videoController;
ChewieController? _chewieController;
@override
void initState() {
super.initState();
_videoController = VideoPlayerController.network(widget.url)
..initialize().then((_) {
setState(() {
_chewieController = ChewieController(
videoPlayerController: _videoController,
autoPlay: true,
looping: false,
);
});
});
}
@override
void dispose() {
_videoController.dispose();
_chewieController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Video")),
body: Center(
child: _chewieController == null
? const CircularProgressIndicator()
: Chewie(controller: _chewieController!),
),
);
}
}

View File

@@ -1,12 +1,386 @@
import 'package:flutter/material.dart';
import 'package: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,
),
],
),
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,62 @@
import 'dart:convert';
import 'package:pusher_channels_flutter/pusher_channels_flutter.dart';
import '../services/dio_client.dart';
class ChatRealtimeService {
static final PusherChannelsFlutter _pusher =
PusherChannelsFlutter.getInstance();
static Future<void> connect({
required int ticketId,
required Function(Map<String, dynamic>) onMessage,
}) async {
print(" 🧪🧪🧪🧪🧪🧪🧪🧪🧪SUBSCRIBING TO: private-ticket.$ticketId");
await _pusher.init(
apiKey: "q5fkk5rvcnatvbgadwvl",
cluster: "mt1",
onAuthorizer: (channelName, socketId, options) async {
print("🧪🧪🧪🧪🧪🧪🧪🧪🧪🧪AUTHORIZING CHANNEL: $channelName");
final dio = DioClient.getInstance(options);
final res = await dio.post(
'/broadcasting/auth',
data: {
'socket_id': socketId,
'channel_name': channelName,
},
);
return res.data;
},
onEvent: (event) {
if (event.eventName == "NewChatMessage") {
final data = jsonDecode(event.data);
onMessage(Map<String, dynamic>.from(data));
}
},
onConnectionStateChange: (current, previous) {
print("PUSHER STATE: $current");
},
onError: (message, code, error) {
print("PUSHER ERROR: $message");
},
);
await _pusher.subscribe(
channelName: "private-ticket.$ticketId",
);
await _pusher.connect();
}
static Future<void> disconnect() async {
await _pusher.disconnect();
}
}

View File

@@ -0,0 +1,69 @@
import 'dart:io';
import 'package:dio/dio.dart';
import '../config/api_config.dart';
class ChatService {
final Dio dio;
ChatService(this.dio);
/// Start chat / get ticket
Future<Map<String, dynamic>> startChat() async {
final res = await dio.get('/user/chat/start');
return Map<String, dynamic>.from(res.data);
}
/// Get all messages
Future<List<dynamic>> getMessages(int ticketId) async {
final res = await dio.get('/user/chat/messages/$ticketId');
return res.data['messages'];
}
/// Send message (text or file)
Future<void> sendMessage(
int ticketId, {
String? message,
String? clientId,
}) async {
final form = FormData();
if (message != null) form.fields.add(MapEntry('message', message));
if (clientId != null) form.fields.add(MapEntry('client_id', clientId));
await dio.post('/user/chat/send/$ticketId', data: form);
}
// ---------------------------
// SEND FILE (image/video/pdf/excel)
// ---------------------------
Future<Map<String, dynamic>> sendFile(
int ticketId,
File file, {
required Function(double) onProgress,
}) async {
final formData = FormData.fromMap({
'file': await MultipartFile.fromFile(
file.path,
filename: file.path.split('/').last,
),
});
final res = await dio.post(
"/user/chat/send/$ticketId",
data: formData,
options: Options(
headers: {'Content-Type': 'multipart/form-data'},
),
onSendProgress: (sent, total) {
if (total > 0) {
onProgress(sent / total);
}
},
);
return Map<String, dynamic>.from(res.data['message']);
}
}

View File

@@ -6,7 +6,10 @@ import '../providers/auth_provider.dart';
import 'token_interceptor.dart';
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) {

View File

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

View File

@@ -0,0 +1,210 @@
import 'dart:async';
import 'dart:convert';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:flutter/material.dart';
import '../services/dio_client.dart';
class ReverbSocketService {
WebSocketChannel? _channel;
Timer? _pingTimer;
bool _connected = false;
late BuildContext _context;
late int _ticketId;
late Function(Map<String, dynamic>) _onMessage;
late VoidCallback _onBackgroundMessage;
late VoidCallback _onAdminMessage;
/// Prevent duplicate messages on reconnect
final Set<int> _receivedIds = {};
// ============================
// CONNECT
// ============================
Future<void> connect({
required BuildContext context,
required int ticketId,
required Function(Map<String, dynamic>) onMessage,
//required VoidCallback onBackgroundMessage,
required VoidCallback onAdminMessage, // 👈 ADD
}) async {
_context = context;
_ticketId = ticketId;
_onMessage = onMessage;
//_onBackgroundMessage = onBackgroundMessage;
_onAdminMessage = onAdminMessage; // 👈 SAVE
final uri = Uri.parse(
'ws://10.119.0.74:8080/app/q5fkk5rvcnatvbgadwvl'
'?protocol=7&client=flutter&version=1.0',
);
// final uri = Uri.parse(
// 'ws://103.248.30.24:8080/app/q5fkk5rvcnatvbgadwvl'
// '?protocol=7&client=flutter&version=1.0',
// );
debugPrint("🔌 CONNECTING SOCKET → $uri");
_channel = WebSocketChannel.connect(uri);
_channel!.stream.listen(
_handleMessage,
onDone: _handleDisconnect,
onError: (e) {
debugPrint("❌ SOCKET ERROR: $e");
_handleDisconnect();
},
);
}
// ============================
// HANDLE SOCKET MESSAGE
// ============================
Future<void> _handleMessage(dynamic raw) async {
debugPrint("📥 RAW: $raw");
final payload = jsonDecode(raw);
final event = payload['event']?.toString() ?? '';
// --------------------------------
// CONNECTED
// --------------------------------
if (event == 'pusher:connection_established') {
final socketId = jsonDecode(payload['data'])['socket_id'];
debugPrint("✅ CONNECTED | socket_id=$socketId");
await _subscribe(socketId);
_startHeartbeat();
_connected = true;
return;
}
// --------------------------------
// HEARTBEAT
// --------------------------------
if (event == 'pusher:pong') {
debugPrint("💓 PONG");
return;
}
// --------------------------------
// CHAT MESSAGE
// --------------------------------
if (
event == 'NewChatMessage' ||
event.endsWith('.NewChatMessage') ||
event.contains('NewChatMessage')) {
dynamic data = payload['data'];
if (data is String) data = jsonDecode(data);
final int msgId = data['id'];
final String senderType = data['sender_type'] ?? '';
final String? incomingClientId = data['client_id']; // ✅ HERE
// 🔁 Prevent duplicates by DB id
if (_receivedIds.contains(msgId)) {
debugPrint("🔁 DUPLICATE MESSAGE IGNORED: $msgId");
return;
}
_receivedIds.add(msgId);
debugPrint("📩 NEW MESSAGE");
debugPrint("🆔 id=$msgId");
debugPrint("🧩 client_id=$incomingClientId");
debugPrint("👤 sender=$senderType");
debugPrint("💬 text=${data['message']}");
// ✅ Forward FULL payload (with client_id) to UI
_onMessage(Map<String, dynamic>.from(data));
// 🔔 Unread count only for admin messages
if (senderType == 'App\\Models\\Admin') {
debugPrint("🔔 ADMIN MESSAGE → UNREAD +1");
_onAdminMessage();
}
return;
}
}
// ============================
// SUBSCRIBE PRIVATE CHANNEL
// ============================
Future<void> _subscribe(String socketId) async {
debugPrint("📡 SUBSCRIBING private-ticket.$_ticketId");
final dio = DioClient.getInstance(_context);
final res = await dio.post(
'/broadcasting/auth',
data: {
'socket_id': socketId,
'channel_name': 'private-ticket.$_ticketId',
},
);
_channel!.sink.add(jsonEncode({
'event': 'pusher:subscribe',
'data': {
'channel': 'private-ticket.$_ticketId',
'auth': res.data['auth'],
},
}));
debugPrint("✅ SUBSCRIBED private-ticket.$_ticketId");
}
// ============================
// HEARTBEAT
// ============================
void _startHeartbeat() {
_pingTimer?.cancel();
_pingTimer = Timer.periodic(
const Duration(seconds: 30),
(_) {
if (_connected) {
debugPrint("💓 SENDING PING");
_channel?.sink.add(jsonEncode({
'event': 'pusher:ping',
'data': {}
}));
}
},
);
}
// ============================
// HANDLE DISCONNECT
// ============================
void _handleDisconnect() {
debugPrint("⚠️ SOCKET DISCONNECTED → RECONNECTING");
_connected = false;
_pingTimer?.cancel();
_channel?.sink.close();
Future.delayed(const Duration(seconds: 2), () {
connect(
context: _context,
ticketId: _ticketId,
onMessage: _onMessage,
// onBackgroundMessage: _onBackgroundMessage,
onAdminMessage: _onAdminMessage, // 👈 ADD
);
});
}
// ============================
// MANUAL DISCONNECT
// ============================
void disconnect() {
debugPrint("❌ SOCKET CLOSED MANUALLY");
_connected = false;
_pingTimer?.cancel();
_channel?.sink.close();
}
}

View File

@@ -1,37 +1,130 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package: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;
}
}
}

View File

@@ -0,0 +1,118 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';
import '../screens/chat_file_viewer.dart';
class ChatFileActions {
static void show(
BuildContext context, {
required String url,
required String fileType,
}) {
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (_) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_actionTile(
icon: Icons.open_in_new,
title: "Open",
onTap: () {
Navigator.pop(context);
ChatFileViewer.open(
context,
url: url,
fileType: fileType,
);
},
),
_actionTile(
icon: Icons.download,
title: "Download",
onTap: () async {
Navigator.pop(context);
await _downloadFile(context, url, fileType);
},
),
const SizedBox(height: 8),
],
),
);
},
);
}
static Widget _actionTile({
required IconData icon,
required String title,
required VoidCallback onTap,
}) {
return ListTile(
leading: Icon(icon),
title: Text(title),
onTap: onTap,
);
}
static Future<void> _scanFile(String path) async {
const platform = MethodChannel('media_scanner');
try {
await platform.invokeMethod('scanFile', {'path': path});
} catch (_) {}
}
// ===========================
// DOWNLOAD LOGIC
// ===========================
static Future<void> _downloadFile(
BuildContext context,
String url,
String fileType,
) async {
try {
final fileName = url.split('/').last;
Directory baseDir;
if (fileType.startsWith('image/')) {
baseDir = Directory('/storage/emulated/0/Pictures/KentChat');
} else if (fileType.startsWith('video/')) {
baseDir = Directory('/storage/emulated/0/Movies/KentChat');
} else {
baseDir = Directory('/storage/emulated/0/Download/KentChat');
}
if (!await baseDir.exists()) {
await baseDir.create(recursive: true);
}
final filePath = "${baseDir.path}/$fileName";
await Dio().download(url, filePath);
// 🔔 IMPORTANT: Tell Android to scan the file
await _scanFile(filePath);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Saved to ${baseDir.path}")),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Download failed")),
);
}
}
}

View File

@@ -0,0 +1,127 @@
import 'dart:io';
import 'package:flutter/material.dart';
import '../services/dio_client.dart';
import '../screens/chat_file_viewer.dart';
import '../widgets/chat_file_actions.dart';
class ChatFilePreview extends StatelessWidget {
final String filePath;
final String fileType;
final bool isUser;
final bool isLocal;
const ChatFilePreview({
super.key,
required this.filePath,
required this.fileType,
required this.isUser,
this.isLocal = false,
});
@override
Widget build(BuildContext context) {
final url = "${DioClient.baseUrl}/storage/$filePath";
final textColor = isUser ? Colors.white : Colors.black;
// LOCAL IMAGE (uploading preview)
if (isLocal && fileType.startsWith('image/')) {
return Image.file(
File(filePath),
width: 180,
height: 120,
fit: BoxFit.cover,
);
}
return GestureDetector(
onTap: () {
// ✅ TAP = OPEN
ChatFileViewer.open(
context,
url: url,
fileType: fileType,
);
},
onLongPress: () {
// ✅ LONG PRESS = OPEN / DOWNLOAD
ChatFileActions.show(
context,
url: url,
fileType: fileType,
);
},
child: _buildPreviewUI(textColor, url),
);
}
Widget _buildPreviewUI(Color textColor, String url) {
// IMAGE
if (fileType.startsWith('image/')) {
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
url,
width: 180,
height: 120,
fit: BoxFit.cover,
),
);
}
// VIDEO
if (fileType.startsWith('video/')) {
return Stack(
alignment: Alignment.center,
children: [
Container(
width: 180,
height: 120,
decoration: BoxDecoration(
color: Colors.black26,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.videocam,
size: 50,
color: Colors.white70,
),
),
const Icon(
Icons.play_circle_fill,
size: 56,
color: Colors.white,
),
],
);
}
// PDF / OTHER FILES
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(_fileIcon(fileType), color: textColor),
const SizedBox(width: 8),
Text(
_fileLabel(fileType),
style: TextStyle(color: textColor),
),
],
);
}
IconData _fileIcon(String type) {
if (type == 'application/pdf') return Icons.picture_as_pdf;
if (type.contains('excel')) return Icons.table_chart;
return Icons.insert_drive_file;
}
String _fileLabel(String type) {
if (type == 'application/pdf') return "PDF document";
if (type.contains('excel')) return "Excel file";
return "Download file";
}
}

View File

@@ -0,0 +1,356 @@
import 'package:flutter/material.dart';
class InvoiceDetailView extends StatefulWidget {
final Map invoice;
const InvoiceDetailView({super.key, required this.invoice});
@override
State<InvoiceDetailView> createState() => _InvoiceDetailViewState();
}
class _InvoiceDetailViewState extends State<InvoiceDetailView>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Offset> _slideAnimation;
bool s1 = true;
bool s2 = false;
bool s3 = false;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 350),
);
_slideAnimation = Tween(
begin: const Offset(0, -0.1),
end: Offset.zero,
).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
// ------------------------------------------------------------------
// STATUS BADGE (WHITE + GRADIENT BORDER)
// ------------------------------------------------------------------
Widget statusBadge(String status, double scale) {
final s = status.toLowerCase();
LinearGradient gradient;
Color textColor;
switch (s) {
case 'paid':
gradient = const LinearGradient(
colors: [Color(0xFF2ECC71), Color(0xFF27AE60)],
);
textColor = const Color(0xFF27AE60);
break;
case 'pending':
gradient = const LinearGradient(
colors: [
Color(0xFF5B8DEF), // Soft Blue
Color(0xFF7B5CFA), // Purple Blue
],
);
textColor = const Color(0xFF5B8DEF);
break;
case 'overdue':
gradient = const LinearGradient(
colors: [
Color(0xFFFFB300), // Amber
Color(0xFFFF6F00), // Deep Orange
],
);
textColor = const Color(0xFFFF6F00);
break;
case 'cancelled':
case 'failed':
gradient = const LinearGradient(
colors: [Color(0xFFE74C3C), Color(0xFFC0392B)],
);
textColor = const Color(0xFFE74C3C);
break;
default:
gradient = const LinearGradient(
colors: [Color(0xFF95A5A6), Color(0xFF7F8C8D)],
);
textColor = Colors.grey.shade700;
}
return Container(
padding: const EdgeInsets.all(1.5),
decoration: BoxDecoration(
gradient: gradient,
borderRadius: BorderRadius.circular(20 * scale),
),
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 14 * scale,
vertical: 6 * scale,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18 * scale),
),
child: Text(
status.toUpperCase(),
style: TextStyle(
fontSize: 12 * scale,
fontWeight: FontWeight.w700,
color: textColor,
letterSpacing: .6,
),
),
),
);
}
// ------------------------------------------------------------------
// HEADER CARD
// ------------------------------------------------------------------
Widget headerCard(Map invoice, double scale) {
return Container(
width: double.infinity,
padding: EdgeInsets.all(16 * scale),
margin: EdgeInsets.only(bottom: 14 * scale),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade400, Colors.indigo.shade600],
),
borderRadius: BorderRadius.circular(16 * scale),
boxShadow: [
BoxShadow(
blurRadius: 8 * scale,
offset: Offset(0, 3 * scale),
color: Colors.indigo.withOpacity(.3),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Invoice #${invoice['invoice_number']}",
style: TextStyle(
fontSize: 20 * scale,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 4 * scale),
Text(
"Date: ${invoice['invoice_date']}",
style: TextStyle(color: Colors.white70, fontSize: 13 * scale),
),
SizedBox(height: 10 * scale),
/// STATUS BADGE
statusBadge(invoice['status'] ?? 'Unknown', scale),
],
),
);
}
// ------------------------------------------------------------------
// SECTION HEADER
// ------------------------------------------------------------------
Widget sectionHeader(
String title,
IconData icon,
bool expanded,
VoidCallback tap,
double scale,
) {
return GestureDetector(
onTap: tap,
child: Container(
padding: EdgeInsets.all(12 * scale),
margin: EdgeInsets.only(bottom: 8 * scale),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.indigo.shade500, Colors.blue.shade400],
),
borderRadius: BorderRadius.circular(12 * scale),
boxShadow: [
BoxShadow(
blurRadius: 5 * scale,
color: Colors.black.withOpacity(.15),
offset: Offset(0, 2 * scale),
)
],
),
child: Row(
children: [
Icon(icon, color: Colors.white, size: 20 * scale),
SizedBox(width: 8 * scale),
Text(
title,
style: TextStyle(
fontSize: 15 * scale,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
AnimatedRotation(
turns: expanded ? .5 : 0,
duration: const Duration(milliseconds: 250),
child: Icon(
Icons.keyboard_arrow_down,
color: Colors.white,
size: 20 * scale,
),
)
],
),
),
);
}
// ------------------------------------------------------------------
// SECTION BODY
// ------------------------------------------------------------------
Widget sectionBody(bool visible, List<Widget> children, double scale) {
return AnimatedCrossFade(
duration: const Duration(milliseconds: 250),
crossFadeState:
visible ? CrossFadeState.showSecond : CrossFadeState.showFirst,
firstChild: const SizedBox.shrink(),
secondChild: SlideTransition(
position: _slideAnimation,
child: Container(
padding: EdgeInsets.all(14 * scale),
margin: EdgeInsets.only(bottom: 10 * scale),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12 * scale),
boxShadow: [
BoxShadow(
blurRadius: 7 * scale,
offset: Offset(0, 2 * scale),
color: Colors.black.withOpacity(.07),
)
],
),
child: Column(children: children),
),
),
);
}
// ------------------------------------------------------------------
// DETAIL ROW
// ------------------------------------------------------------------
Widget detailRow(IconData icon, String label, dynamic value, double scale) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 5 * scale),
child: Row(
children: [
Icon(icon, size: 18 * scale, color: Colors.blueGrey),
SizedBox(width: 8 * scale),
Expanded(
flex: 3,
child: Text(
label,
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 13 * scale,
),
),
),
Expanded(
flex: 4,
child: label == "Status"
? Align(
alignment: Alignment.centerRight,
child: statusBadge(value ?? 'Unknown', scale),
)
: Text(
value?.toString() ?? "N/A",
textAlign: TextAlign.right,
style: TextStyle(
fontSize: 14 * scale,
fontWeight: FontWeight.bold,
),
),
)
],
),
);
}
// ------------------------------------------------------------------
// MAIN BUILD
// ------------------------------------------------------------------
@override
Widget build(BuildContext context) {
final invoice = widget.invoice;
final width = MediaQuery.of(context).size.width;
final scale = (width / 390).clamp(0.85, 1.25);
return ListView(
padding: EdgeInsets.all(14 * scale),
children: [
headerCard(invoice, scale),
sectionHeader(
"Invoice Summary",
Icons.receipt_long,
s1,
() => setState(() => s1 = !s1),
scale,
),
sectionBody(s1, [
detailRow(Icons.numbers, "Invoice No", invoice['invoice_number'], scale),
detailRow(Icons.calendar_today, "Date", invoice['invoice_date'], scale),
detailRow(Icons.label, "Status", invoice['status'], scale),
], scale),
sectionHeader(
"Customer Details",
Icons.person,
s2,
() => setState(() => s2 = !s2),
scale,
),
sectionBody(s2, [
detailRow(Icons.person_outline, "Name", invoice['customer_name'], scale),
detailRow(Icons.mail, "Email", invoice['customer_email'], scale),
detailRow(Icons.phone, "Phone", invoice['customer_mobile'], scale),
detailRow(Icons.location_on, "Address", invoice['customer_address'], scale),
], scale),
sectionHeader(
"Amount Details",
Icons.currency_rupee,
s3,
() => setState(() => s3 = !s3),
scale,
),
sectionBody(s3, [
detailRow(Icons.money, "Amount", invoice['final_amount'], scale),
detailRow(Icons.percent, "GST %", invoice['gst_percent'], scale),
detailRow(Icons.percent, "GST Amount", invoice['gst_amount'], scale),
detailRow(Icons.summarize, "Total", invoice['final_amount_with_gst'], scale),
], scale),
],
);
}
}

View File

@@ -13,7 +13,7 @@ class MainAppBar extends StatelessWidget implements PreferredSizeWidget {
final profileUrl = profileProvider.profile?.profileImage;
return AppBar(
backgroundColor: Colors.lightGreen,
backgroundColor: Colors.white,
elevation: 0.8,
surfaceTintColor: Colors.transparent,
automaticallyImplyLeading: false,

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

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

View File

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

View File

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

View File

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

View File

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