Compare commits

...

7 Commits

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
29 changed files with 1693 additions and 916 deletions

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,13 @@
<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"

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

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.11.236.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.11.236.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

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

@@ -176,17 +176,17 @@ class InstallmentCard extends StatelessWidget {
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,
),
),
),
// Align(
// alignment: Alignment.centerRight,
// child: Text(
// "Installment #${inst['id'] ?? ''}",
// style: TextStyle(
// fontSize: isTablet ? 15 : 13,
// color: Colors.grey.shade600,
// fontWeight: FontWeight.w500,
// ),
// ),
// ),
],
),
),

View File

@@ -12,8 +12,6 @@ import 'chat_file_viewer.dart';
import '../widgets/chat_file_preview.dart';
class ChatScreen extends StatefulWidget {
const ChatScreen({super.key});
@@ -24,6 +22,7 @@ class ChatScreen extends StatefulWidget {
class _ChatScreenState extends State<ChatScreen> {
final TextEditingController _messageCtrl = TextEditingController();
final ScrollController _scrollCtrl = ScrollController();
Map<String, dynamic>? uploadingMessage;
late ChatService _chatService;
final ReverbSocketService _socket = ReverbSocketService();
@@ -48,17 +47,50 @@ class _ChatScreenState extends State<ChatScreen> {
_initChat();
}
Future<void> _pickAndSendFile() async {
final XFile? file = await openFile();
if (file == null || ticketId == null) return;
final dartFile = File(file.path);
await _chatService.sendFile(ticketId!, dartFile);
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
@@ -77,10 +109,22 @@ class _ChatScreenState extends State<ChatScreen> {
context: context,
ticketId: ticketId!,
onMessage: (msg) {
if (!mounted) return;
setState(() => messages.add(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();
@@ -88,9 +132,6 @@ class _ChatScreenState extends State<ChatScreen> {
},
);
if (!mounted) return;
setState(() => isLoading = false);
_scrollToBottom();
@@ -112,13 +153,31 @@ class _ChatScreenState extends State<ChatScreen> {
// ============================
Future<void> _sendMessage() async {
final text = _messageCtrl.text.trim();
if (text.isEmpty) return;
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,
);
}
@@ -154,22 +213,6 @@ class _ChatScreenState extends State<ChatScreen> {
);
}
// Future<void> _openUrl(String url) async {
// final uri = Uri.parse(url);
//
// if (await canLaunchUrl(uri)) {
// await launchUrl(
// uri,
// mode: LaunchMode.externalApplication,
// );
// } else {
// debugPrint("❌ Cannot launch URL: $url");
// }
// }
Widget _buildMessageContent({
String? message,
String? filePath,
@@ -222,48 +265,96 @@ class _ChatScreenState extends State<ChatScreen> {
return "Download file";
}
Widget _buildMessages() {
return ListView.builder(
return ListView(
controller: _scrollCtrl,
padding: const EdgeInsets.all(12),
itemCount: messages.length,
itemBuilder: (_, index) {
final msg = messages[index];
final isUser = msg['sender_type'] == 'App\\Models\\User';
children: [
// EXISTING MESSAGES
...messages.map((msg) {
final isUser = msg['sender_type'] == 'App\\Models\\User';
final String? filePath = msg['file_path'];
final String? fileType = msg['file_type'];
final String? message = msg['message'];
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),
),
// 🔽 ONLY THIS PART CHANGED
child: filePath == null
? Text(
message ?? '',
style: TextStyle(
color: isUser ? Colors.white : Colors.black,
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,
),
),
],
),
)
: ChatFilePreview(
filePath: filePath,
fileType: fileType ?? '',
isUser: isUser,
),
),
);
},
],
);
}
@@ -292,5 +383,4 @@ class _ChatScreenState extends State<ChatScreen> {
),
);
}
}

View File

@@ -35,7 +35,6 @@ class _DashboardScreenState extends State<DashboardScreen>
WidgetsBinding.instance.addPostFrameCallback((_) async {
final auth = Provider.of<AuthProvider>(context, listen: false);
await auth.tryRefreshToken(context);
final dash = Provider.of<DashboardProvider>(context, listen: false);
dash.init(context);
@@ -542,7 +541,22 @@ class _DashboardScreenState extends State<DashboardScreen>
vertical: 6 * scale,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(.2),
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(
@@ -553,7 +567,7 @@ class _DashboardScreenState extends State<DashboardScreen>
fontWeight: FontWeight.bold,
),
),
),
)
],
),
);

View File

@@ -1,9 +1,12 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:share_plus/share_plus.dart';
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';
@@ -26,9 +29,6 @@ class _InvoiceDetailScreenState extends State<InvoiceDetailScreen> {
load();
}
// -------------------------------------------------------
// ⭐ LOAD INVOICE FROM API
// -------------------------------------------------------
Future<void> load() async {
final service = InvoiceService(DioClient.getInstance(context));
try {
@@ -40,93 +40,116 @@ class _InvoiceDetailScreenState extends State<InvoiceDetailScreen> {
invoice = {};
}
} catch (e) {
// handle error gracefully
invoice = {};
} finally {
if (mounted) setState(() => loading = false);
}
}
// -------------------------------------------------------
// ⭐ GENERATE + SAVE PDF TO DOWNLOADS FOLDER
// (No Permission Needed)
// -------------------------------------------------------
Future<File> generatePDF() async {
final pdf = pw.Document();
String? get pdfUrl {
final path = invoice['pdf_path'];
if (path == null || path.toString().isEmpty) return null;
pdf.addPage(
pw.Page(
build: (context) => pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(
"INVOICE DETAILS",
style: pw.TextStyle(fontSize: 26, fontWeight: pw.FontWeight.bold),
),
pw.SizedBox(height: 20),
return ApiConfig.fileBaseUrl + path;
}
pw.Text("Invoice ID: ${invoice['id'] ?? '-'}"),
pw.Text("Amount: ₹${invoice['amount'] ?? '-'}"),
pw.Text("Status: ${invoice['status'] ?? '-'}"),
pw.Text("Date: ${invoice['date'] ?? '-'}"),
pw.Text("Customer: ${invoice['customer_name'] ?? '-'}"),
],
),
),
);
static const MethodChannel _mediaScanner =
MethodChannel('media_scanner');
// ⭐ SAFEST WAY (Android 1014)
final downloadsDir = await getDownloadsDirectory();
final filePath = "${downloadsDir!.path}/invoice_${invoice['id']}.pdf";
final file = File(filePath);
await file.writeAsBytes(await pdf.save());
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("PDF saved to Downloads:\n$filePath")),
);
Future<void> _scanFile(String path) async {
try {
await _mediaScanner.invokeMethod('scanFile', {'path': path});
} catch (e) {
debugPrint("❌ MediaScanner error: $e");
}
return file;
}
// -------------------------------------------------------
// ⭐ SHARE THE SAVED PDF FILE
// -------------------------------------------------------
Future<void> sharePDF() async {
final file = await generatePDF();
await Share.shareXFiles(
[XFile(file.path)],
text: "Invoice Details",
);
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: Text(
"Invoice Details",
style: TextStyle(
fontSize: 18 * scale,
fontWeight: FontWeight.w600,
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")),
);
}
},
),
),
// ⭐ PDF + SHARE BUTTONS
actions: [
// SHARE
IconButton(
icon: const Icon(Icons.picture_as_pdf),
onPressed: generatePDF,
),
IconButton(
icon: const Icon(Icons.share),
onPressed: sharePDF,
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']}",
);
}
},
),
],
),
@@ -146,8 +169,10 @@ class _InvoiceDetailScreenState extends State<InvoiceDetailScreen> {
)
: 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

@@ -24,19 +24,16 @@ class _MarkListScreenState extends State<MarkListScreen> {
Widget build(BuildContext context) {
final marks = Provider.of<MarkListProvider>(context);
// Responsive scale factor
final screenWidth = MediaQuery.of(context).size.width;
final scale = (screenWidth / 390).clamp(0.82, 1.35);
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
centerTitle: true,
iconTheme: const IconThemeData(color: Colors.black),
title: Text(
"All Mark Numbers",
style: TextStyle(
@@ -50,14 +47,31 @@ class _MarkListScreenState extends State<MarkListScreen> {
body: marks.loading
? const Center(child: CircularProgressIndicator())
: ListView.builder(
padding: EdgeInsets.all(10 * scale), // smaller padding
padding: EdgeInsets.all(10 * scale),
itemCount: marks.marks.length,
itemBuilder: (_, i) {
final m = marks.marks[i];
final status =
(m['status'] ?? '').toString().toLowerCase();
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), // reduced margin
padding: EdgeInsets.all(12 * scale), // smaller padding
margin: EdgeInsets.only(bottom: 10 * scale),
padding: EdgeInsets.all(12 * scale),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [
@@ -67,16 +81,15 @@ class _MarkListScreenState extends State<MarkListScreen> {
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(14 * scale), // smaller radius
borderRadius: BorderRadius.circular(14 * scale),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.06),
blurRadius: 5 * scale, // smaller shadow
blurRadius: 5 * scale,
offset: Offset(0, 2 * scale),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -85,24 +98,20 @@ class _MarkListScreenState extends State<MarkListScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// MARK NUMBER
Text(
m['mark_no'],
style: TextStyle(
color: Colors.white,
fontSize: 16 * scale, // reduced font
fontSize: 16 * scale,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 3 * scale),
// ROUTE
Text(
"${m['origin']}${m['destination']}",
style: TextStyle(
color: Colors.white,
fontSize: 13 * scale, // reduced font
fontSize: 13 * scale,
fontWeight: FontWeight.w500,
),
),
@@ -110,21 +119,21 @@ class _MarkListScreenState extends State<MarkListScreen> {
),
),
// STATUS BADGE
// STATUS BADGE (GREEN / RED)
Container(
padding: EdgeInsets.symmetric(
horizontal: 10 * scale,
vertical: 5 * scale, // smaller badge
vertical: 5 * scale,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.92),
borderRadius: BorderRadius.circular(24 * scale),
gradient: statusGradient,
borderRadius: BorderRadius.circular(20 * scale),
),
child: Text(
m['status'],
style: TextStyle(
fontSize: 11.5 * scale,
color: Colors.black87,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),

View File

@@ -16,6 +16,12 @@ class _OrderDetailScreenState extends State<OrderDetailScreen> {
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() {
super.initState();
@@ -65,6 +71,10 @@ class _OrderDetailScreenState extends State<OrderDetailScreen> {
_itemsSection(items, scale),
SizedBox(height: 18 * scale),
_totalsSection(scale),
SizedBox(height: 24 * scale),
_confirmOrderButton(scale),
],
),
),
@@ -72,6 +82,69 @@ class _OrderDetailScreenState extends State<OrderDetailScreen> {
);
}
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
// -----------------------------
@@ -267,7 +340,7 @@ class _OrderDetailScreenState extends State<OrderDetailScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Totals",
Text("Totalss",
style: TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
SizedBox(height: 12 * scale),
_totalRow("Total Qty", order['ttl_qty'], scale),

View File

@@ -84,7 +84,6 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
children: [
_headerCard(scale),
// SUMMARY SECTION
_sectionHeader("Invoice Summary", Icons.receipt, s1, () {
setState(() => s1 = !s1);
}, scale),
@@ -92,23 +91,19 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
_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),
_detailRow(Icons.payment, "Payment Method", invoice['payment_method'], scale),
_detailRow(Icons.confirmation_number, "Reference No",
invoice['reference_no'], scale),
], scale),
// AMOUNT SECTION
_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),
// CUSTOMER SECTION
_sectionHeader("Customer Details", Icons.person, s3, () {
setState(() => s3 = !s3);
}, scale),
@@ -120,7 +115,6 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
_detailRow(Icons.location_on, "Address", invoice['customer_address'], scale),
], scale),
// ITEMS SECTION
_sectionHeader("Invoice Items", Icons.shopping_cart, s4, () {
setState(() => s4 = !s4);
}, scale),
@@ -138,44 +132,63 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
// ---------------- HEADER CARD ----------------
Widget _headerCard(double scale) {
final statusColor = getInvoiceStatusColor(invoice["status"]);
return Container(
padding: EdgeInsets.all(18 * scale),
margin: EdgeInsets.only(bottom: 18 * scale),
decoration: BoxDecoration(
gradient:
LinearGradient(colors: [Colors.indigo.shade400, Colors.blue.shade600]),
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))
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)),
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)),
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.withOpacity(.2),
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"]?.toString() ?? "Unknown",
style: TextStyle(color: Colors.white, fontSize: 14 * scale),
(invoice["status"] ?? "Unknown").toString().toUpperCase(),
style: TextStyle(
color: statusColor, // ✅ Text color changes
fontSize: 14 * scale,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
)
],
),
);
@@ -198,11 +211,14 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
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)),
Text(
title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15 * scale,
color: Colors.white,
),
),
const Spacer(),
AnimatedRotation(
turns: expanded ? .5 : 0,
@@ -233,9 +249,10 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
color: Colors.white,
boxShadow: [
BoxShadow(
blurRadius: 8 * scale,
offset: Offset(0, 3 * scale),
color: Colors.black.withOpacity(.08)),
blurRadius: 8 * scale,
offset: Offset(0, 3 * scale),
color: Colors.black.withOpacity(.08),
),
],
),
child: Column(children: children),
@@ -255,16 +272,19 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
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)),
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),
fontWeight: FontWeight.bold,
fontSize: 15 * scale,
),
),
)
],
@@ -286,16 +306,16 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
color: Colors.white,
boxShadow: [
BoxShadow(
blurRadius: 8 * scale,
offset: Offset(0, 3 * scale),
color: Colors.black.withOpacity(.08)),
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: [
// TITLE
Row(
children: [
Container(
@@ -312,15 +332,14 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
child: Text(
item['description'] ?? "Item",
style: TextStyle(
fontSize: 16 * scale, fontWeight: FontWeight.w600),
fontSize: 16 * scale,
fontWeight: FontWeight.w600,
),
),
),
],
),
SizedBox(height: 14 * scale),
// QTY & PRICE
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -328,10 +347,7 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
_itemBadge(Icons.currency_rupee, "Price", "$price", false, scale),
],
),
SizedBox(height: 12 * scale),
// TOTAL
Container(
padding: EdgeInsets.symmetric(
vertical: 10 * scale, horizontal: 14 * scale),
@@ -383,8 +399,9 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
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),
color: highlight ? Colors.indigo : Colors.grey.shade300,
width: highlight ? 1.5 * scale : 1,
),
),
child: Row(
children: [
@@ -413,3 +430,22 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
);
}
}
// ---------------- 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

@@ -32,68 +32,24 @@ class _OrdersScreenState extends State<OrdersScreen> {
}
final screenWidth = MediaQuery.of(context).size.width;
final scale = (screenWidth / 420).clamp(0.85, 1.15);
final scale = (screenWidth / 420).clamp(0.85, 1.1);
// FILTER ORDERS
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) ||
o["amount"].toString().toLowerCase().contains(q);
o["description"].toString().toLowerCase().contains(q);
}).toList();
return Column(
children: [
// ⭐⭐ WHITE ELEVATED SEARCH BAR ⭐⭐
Container(
margin: EdgeInsets.fromLTRB(16 * scale, 16 * scale, 16 * scale, 10 * scale),
padding: EdgeInsets.symmetric(horizontal: 14 * scale),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16 * scale),
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(0.12),
blurRadius: 10 * scale,
spreadRadius: 1 * scale,
offset: Offset(0, 4 * scale),
),
],
),
child: Row(
children: [
Icon(Icons.search,
size: 22 * scale, color: Colors.grey.shade700),
SizedBox(width: 10 * scale),
Expanded(
child: TextField(
onChanged: (value) => setState(() => searchQuery = value),
style: TextStyle(fontSize: 15 * scale),
decoration: InputDecoration(
hintText: "Search orders...",
hintStyle: TextStyle(
fontSize: 14 * scale,
color: Colors.grey.shade600,
),
border: InputBorder.none,
),
),
),
],
),
),
// LIST OF ORDERS
_searchBar(scale),
Expanded(
child: ListView.builder(
padding: EdgeInsets.all(16 * scale),
padding: EdgeInsets.all(14 * scale),
itemCount: filteredOrders.length,
itemBuilder: (context, i) {
final order = filteredOrders[i];
return _orderCard(order, scale);
return _orderCard(filteredOrders[i], scale);
},
),
),
@@ -101,123 +57,175 @@ class _OrdersScreenState extends State<OrdersScreen> {
);
}
// ORDER CARD UI
Widget _searchBar(double scale) {
return Container(
margin: EdgeInsets.fromLTRB(14 * scale, 14 * scale, 14 * scale, 8 * scale),
padding: EdgeInsets.symmetric(horizontal: 12 * scale),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14 * scale),
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(0.1),
blurRadius: 8 * scale,
offset: const Offset(0, 3),
),
],
),
child: Row(
children: [
Icon(Icons.search, size: 20 * scale, color: Colors.grey.shade700),
SizedBox(width: 8 * scale),
Expanded(
child: TextField(
onChanged: (v) => setState(() => searchQuery = v),
decoration: const InputDecoration(
hintText: "Search orders...",
border: InputBorder.none,
),
),
),
],
),
);
}
Widget _orderCard(Map<String, dynamic> o, double scale) {
final progress = getProgress(o['status']);
final badgeColor = getStatusColor(o['status']);
final percent = (progress * 100).toInt();
return Card(
elevation: 3 * scale,
elevation: 2,
margin: EdgeInsets.only(bottom: 12 * scale),
color: Colors.white,
margin: EdgeInsets.only(bottom: 16 * scale),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16 * scale),
borderRadius: BorderRadius.circular(14),
),
child: Padding(
padding: EdgeInsets.all(16 * scale),
padding: EdgeInsets.all(12 * scale), // 👈 tighter padding
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// TOP ROW
// HEADER
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Order #${o['order_id']}",
style: TextStyle(
fontSize: 18 * scale,
fontWeight: FontWeight.bold,
),
),
Container(
padding: EdgeInsets.symmetric(
horizontal: 12 * scale,
vertical: 6 * scale,
),
decoration: BoxDecoration(
color: badgeColor.withOpacity(0.15),
borderRadius: BorderRadius.circular(12 * scale),
),
child: Text(
o['status'],
style: TextStyle(
color: badgeColor,
fontWeight: FontWeight.bold,
fontSize: 13 * scale,
),
fontSize: 15 * scale, // 👈 slightly smaller
fontWeight: FontWeight.w600,
),
),
getStatusBadge(o['status'], scale),
],
),
SizedBox(height: 10 * scale),
SizedBox(height: 6 * scale),
Text(
o['description'],
style: TextStyle(fontSize: 14 * scale),
style: TextStyle(fontSize: 13 * scale),
),
SizedBox(height: 5 * scale),
SizedBox(height: 4 * scale),
Text(
"${o['amount']}",
style: TextStyle(
fontSize: 16 * scale,
fontSize: 15 * scale,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 18 * scale),
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: 18 * scale),
SizedBox(height: 6 * scale),
// BUTTONS
// 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.shade800,
Colors.green.shade50, () => _openOrderDetails(o['order_id']), scale),
_btn(Icons.receipt_long, "Invoice", Colors.orange.shade800,
Colors.orange.shade50, () => _openInvoice(o['order_id']), scale),
_btn(Icons.local_shipping, "Track", Colors.blue.shade800,
Colors.blue.shade50, () => _openTrack(o['order_id']), scale),
_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),
],
)
),
],
),
),
);
}
// BUTTON UI
Widget _btn(IconData icon, String text, Color fg, Color bg,
VoidCallback onTap, double scale) {
Widget _btn(
IconData icon,
String text,
Color fg,
Color bg,
VoidCallback onTap,
double scale,
) {
return InkWell(
borderRadius: BorderRadius.circular(12 * scale),
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: 20 * scale,
vertical: 12 * scale,
horizontal: 16 * scale, // slightly more horizontal space
vertical: 8 * scale,
),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(12 * scale),
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center, // 👈 centers content
children: [
Icon(icon, size: 18 * scale, color: fg),
SizedBox(width: 8 * scale),
Icon(icon, size: 16 * scale, color: fg),
SizedBox(width: 6 * scale),
Text(
text,
style: TextStyle(
color: fg,
fontWeight: FontWeight.w600,
fontSize: 14 * scale,
fontSize: 12 * scale,
),
),
],
@@ -226,30 +234,48 @@ class _OrdersScreenState extends State<OrdersScreen> {
);
}
// NAVIGATION
void _openOrderDetails(String id) {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => OrderDetailScreen(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 _openInvoice(String id) {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => OrderInvoiceScreen(orderId: id)),
);
}
void _openOrderDetails(String id) =>
Navigator.push(context,
MaterialPageRoute(builder: (_) => OrderDetailScreen(orderId: id)));
void _openTrack(String id) {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => OrderTrackScreen(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
// ================= PROGRESS BAR =================
class _AnimatedProgressBar extends StatelessWidget {
final double progress;
final double scale;
@@ -258,63 +284,106 @@ class _AnimatedProgressBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final maxW = constraints.maxWidth;
return Stack(
children: [
Container(
height: 10 * scale,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(20 * scale),
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)],
),
),
AnimatedContainer(
duration: const Duration(milliseconds: 650),
curve: Curves.easeInOut,
height: 10 * scale,
width: maxW * progress,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20 * scale),
gradient: const LinearGradient(
colors: [
Color(0xFF4F8CFF),
Color(0xFF8A4DFF),
],
),
),
),
],
);
},
);
),
],
);
});
}
}
// PROGRESS VALUES
// ================= STATUS LOGIC =================
double getProgress(String? status) {
final s = (status ?? '').toLowerCase();
if (s == "pending") return 0.25;
if (s == "loading") return 0.40;
if (s == "in transit" || s == "intransit") return 0.65;
if (s == "dispatched") return 0.85;
if (s == "delivered") return 1.0;
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;
}
// STATUS COLORS
Color getStatusColor(String? status) {
_StatusConfig statusConfig(String? status) {
final s = (status ?? '').toLowerCase();
if (s == "pending") return Colors.orange;
if (s == "loading") return Colors.amber.shade800;
if (s == "in transit" || s == "intransit") return Colors.red;
if (s == "dispatched") return Colors.blue.shade700;
if (s == "delivered") return Colors.green.shade700;
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 Colors.black54;
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

@@ -3,6 +3,7 @@ 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;
@@ -21,6 +22,7 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
late final AnimationController progressController;
late final AnimationController shipController;
late final AnimationController timelineController;
@override
void initState() {
@@ -36,6 +38,11 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
duration: const Duration(milliseconds: 1600),
)..repeat(reverse: true);
timelineController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
);
WidgetsBinding.instance.addPostFrameCallback((_) => _loadData());
}
@@ -43,6 +50,7 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
void dispose() {
progressController.dispose();
shipController.dispose();
timelineController.dispose();
super.dispose();
}
@@ -56,19 +64,19 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
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}),
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 DATA -------------------
shipment = shipRes["success"] == true
? Map<String, dynamic>.from(shipRes["shipment"] ?? {})
: null;
/// ------------------- TRACKING DATA -------------------
trackData = trackRes["success"] == true
? Map<String, dynamic>.from(trackRes["track"] ?? {})
: {};
@@ -82,6 +90,8 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
target,
curve: Curves.easeInOutCubic,
);
// Animate timeline
timelineController.forward(from: 0);
} catch (_) {}
}
@@ -90,9 +100,8 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
// ---------------- PROGRESS LOGIC ----------------
double _computeProgress() {
final status = (trackData["shipment_status"] ?? "")
.toString()
.toLowerCase();
final status =
(trackData["shipment_status"] ?? "").toString().toLowerCase();
if (status.contains("delivered")) return 1.0;
if (status.contains("dispatched")) return 0.85;
@@ -100,22 +109,9 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
if (status.contains("loading")) return 0.40;
if (status.contains("pending")) return 0.25;
if (_hasTimestamp("delivered_at")) return 1.0;
if (_hasTimestamp("dispatched_at")) return 0.85;
if (_hasTimestamp("in_transit_at")) return 0.65;
if (_hasTimestamp("loading_at")) return 0.40;
if (_hasTimestamp("pending_at")) return 0.25;
return 0.05;
}
bool _hasTimestamp(String key) {
final v = trackData[key];
if (v == null) return false;
final s = v.toString().trim().toLowerCase();
return s.isNotEmpty && s != "null";
}
String _fmt(dynamic v) {
if (v == null) return "-";
try {
@@ -126,6 +122,92 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
}
}
// ---------------- 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) {
@@ -148,21 +230,13 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
SizedBox(height: 16 * scale),
_shipmentSummary(scale),
SizedBox(height: 16 * scale),
_shipmentTotals(scale),
SizedBox(height: 16 * scale),
_shipmentItems(scale), // USING SHIPMENT API ONLY
SizedBox(height: 20 * scale),
_trackingStatus(scale),
SizedBox(height: 16 * scale),
_progressBar(scale, width),
SizedBox(height: 16 * scale),
_detailsCard(scale),
// Replace horizontal progress bar with vertical timeline
_shipmentTimeline(scale),
],
),
),
@@ -223,7 +297,6 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
SizedBox(height: 12 * scale),
// Shipment ID
Container(
padding: EdgeInsets.all(12 * scale),
decoration: BoxDecoration(
@@ -242,7 +315,12 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
),
SizedBox(height: 14 * scale),
_twoCol("Status", shipment!['status'], 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),
@@ -282,142 +360,9 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
);
}
// ---------------- SHIPMENT TOTALS ----------------
Widget _shipmentTotals(double scale) {
if (shipment == null) return const SizedBox.shrink();
return Container(
padding: EdgeInsets.all(14 * scale),
decoration: _boxDecoration(scale),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Totals",
style:
TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
SizedBox(height: 10 * scale),
_twoCol("Total CTN", shipment!['total_ctn'], scale),
_twoCol("Total Qty", shipment!['total_qty'], scale),
_twoCol("Total Amount", shipment!['total_amount'], scale),
_twoCol("Total CBM", shipment!['total_cbm'], scale),
_twoCol("Total KG", shipment!['total_kg'], scale),
],
),
);
}
// ---------------- SHIPMENT ITEMS (FROM SHIPMENT API ONLY) ----------------
Widget _shipmentItems(double scale) {
if (shipment == null) return const SizedBox.shrink();
final items = (shipment!['items'] as List?) ?? [];
return Container(
padding: EdgeInsets.all(14 * scale),
decoration: _boxDecoration(scale),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Shipment Items",
style:
TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
SizedBox(height: 14 * scale),
...items.map((item) {
final orderId = item["order_id"]?.toString() ?? "-";
final qty = item["total_ttl_qty"]?.toString() ?? "-";
final cbm = item["total_ttl_cbm"]?.toString() ?? "-";
final kg = item["total_ttl_kg"]?.toString() ?? "-";
final amount = item["total_amount"]?.toString() ?? "-";
return Card(
elevation: 2,
margin: EdgeInsets.only(bottom: 12 * scale),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14 * scale),
),
child: Padding(
padding: EdgeInsets.all(14 * scale),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
Container(
padding: EdgeInsets.all(8 * scale),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(12 * scale),
),
child: Icon(Icons.inventory_2,
color: Colors.blue, size: 20 * scale),
),
SizedBox(width: 12 * scale),
Text(
"Order ID: $orderId",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15 * scale),
),
],
),
SizedBox(height: 12 * scale),
if (item["mark_no"] != null)
Text("Mark No: ${item["mark_no"]}",
style: TextStyle(fontSize: 13 * scale)),
SizedBox(height: 6 * scale),
Text("Quantity: $qty",
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14 * scale)),
Text("CBM: $cbm", style: TextStyle(fontSize: 13 * scale)),
Text("KG: $kg", style: TextStyle(fontSize: 13 * scale)),
SizedBox(height: 10 * scale),
Container(
padding: EdgeInsets.symmetric(
vertical: 8 * scale, horizontal: 12 * scale),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(10 * scale),
border: Border.all(color: Colors.blue, width: 1),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Icon(Icons.currency_rupee,
color: Colors.blue),
Text(
"Amount: ₹$amount",
style: TextStyle(
fontSize: 15 * scale,
fontWeight: FontWeight.bold,
color: Colors.blue),
),
],
),
),
],
),
),
);
}).toList(),
],
),
);
}
// ---------------- TRACKING STATUS ----------------
Widget _trackingStatus(double scale) {
final p = _computeProgress();
final delivered = p >= 1.0;
final delivered = _computeProgress() >= 1.0;
return Container(
padding:
@@ -438,142 +383,294 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
),
SizedBox(width: 8 * scale),
Text(
(trackData["shipment_status"] ?? "-")
.toString()
.toUpperCase(),
formatStatusLabel(trackData["shipment_status"]),
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 13 * scale),
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 13 * scale,
),
),
],
),
);
}
// ---------------- PROGRESS BAR ----------------
Widget _progressBar(double scale, double width) {
final usableWidth = width - (48 * scale);
// ---------------- VERTICAL SHIPMENT TIMELINE ----------------
Widget _shipmentTimeline(double scale) {
final currentStepIndex = _getCurrentStepIndex();
return Container(
padding: EdgeInsets.all(12 * scale),
padding: EdgeInsets.all(16 * scale),
decoration: _boxDecoration(scale),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Shipment Progress",
style:
TextStyle(fontWeight: FontWeight.bold, fontSize: 15 * scale)),
SizedBox(height: 12 * scale),
Stack(
alignment: Alignment.centerLeft,
Row(
children: [
// Background
Container(
width: usableWidth,
height: 10 * scale,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(20 * scale)),
),
// Progress Bar
AnimatedBuilder(
animation: progressController,
builder: (_, __) {
final w = usableWidth *
progressController.value.clamp(0.0, 1.0);
return Container(
width: w,
height: 10 * scale,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF4F8CFF), Color(0xFF8A4DFF)]),
borderRadius: BorderRadius.circular(20 * scale),
),
);
},
),
// Moving Truck Icon
AnimatedBuilder(
animation: shipController,
builder: (_, __) {
final progress = progressController.value.clamp(0.0, 1.0);
final bob = sin(shipController.value * 2 * pi) * (4 * scale);
return Positioned(
left: (usableWidth - 26 * scale) * progress,
top: -6 * scale + bob,
child: Container(
width: 26 * scale,
height: 26 * scale,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF4F8CFF), Color(0xFF8A4DFF)],
),
borderRadius: BorderRadius.circular(8 * scale),
),
child: Icon(
Icons.local_shipping,
size: 16 * scale,
color: Colors.white,
),
),
);
},
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: 10 * scale),
AnimatedBuilder(
animation: progressController,
builder: (_, __) => Text(
"${(progressController.value * 100).toInt()}% Completed",
style: TextStyle(color: Colors.black54, fontSize: 13 * 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),
),
],
),
);
}
// ---------------- DETAILS CARD ----------------
Widget _detailsCard(double scale) {
Widget _buildTimeline(double scale, int currentStepIndex) {
return Container(
padding: EdgeInsets.all(14 * scale),
decoration: _boxDecoration(scale),
child: Column(
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: [
_detailsRow("Order ID", trackData["order_id"], scale),
SizedBox(height: 10 * scale),
_detailsRow("Status", trackData["shipment_status"], scale),
SizedBox(height: 10 * scale),
_detailsRow("Date", _fmt(trackData["shipment_date"]), scale),
// 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 _detailsRow(String title, dynamic value, double scale) {
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: [
Expanded(
child: Text(title,
style: TextStyle(
fontWeight: FontWeight.w700, fontSize: 14 * scale))),
Expanded(
child: Text(value?.toString() ?? "-",
textAlign: TextAlign.right,
style: TextStyle(color: Colors.black87, fontSize: 14 * scale))),
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,
),
),
],
);
}
// ---------------- BOX DECORATION ----------------
BoxDecoration _boxDecoration(double scale) {
return BoxDecoration(
color: Colors.white,
@@ -587,4 +684,4 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
],
);
}
}
}

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

@@ -24,27 +24,25 @@ class ChatService {
Future<void> sendMessage(
int ticketId, {
String? message,
String? filePath,
String? clientId,
}) async {
final form = FormData();
if (message != null) form.fields.add(MapEntry('message', message));
if (filePath != null) {
form.files.add(
MapEntry(
'file',
await MultipartFile.fromFile(filePath),
),
);
}
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<void> sendFile(int ticketId, File file) async {
Future<Map<String, dynamic>> sendFile(
int ticketId,
File file, {
required Function(double) onProgress,
}) async {
final formData = FormData.fromMap({
'file': await MultipartFile.fromFile(
file.path,
@@ -52,13 +50,20 @@ class ChatService {
),
});
await dio.post(
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

@@ -8,7 +8,8 @@ import 'token_interceptor.dart';
class DioClient {
static Dio? _dio;
static const String baseUrl = "http://10.11.236.74:8000";
// 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

@@ -4,55 +4,30 @@ class InvoiceService {
final Dio dio;
InvoiceService(this.dio);
// -------------------------------------------------------
// ⭐ GET ALL INVOICES
// -------------------------------------------------------
Future<Map<String, dynamic>> getAllInvoices() async {
try {
final res = await dio.get("/user/invoices");
print("🔵 ALL INVOICES RESPONSE:");
print(res.data);
return Map<String, dynamic>.from(res.data);
} catch (e) {
print("❌ ERROR (All Invoices): $e");
return {"success": false, "message": e.toString()};
}
}
// -------------------------------------------------------
// ⭐ GET INSTALLMENTS
// -------------------------------------------------------
Future<Map<String, dynamic>> getInstallments(int invoiceId) async {
try {
final res =
await dio.get("/user/invoice/$invoiceId/installments");
print("🔵 INSTALLMENTS RESPONSE:");
print(res.data);
final res = await dio.get("/user/invoice/$invoiceId/installments");
return Map<String, dynamic>.from(res.data);
} catch (e) {
print("❌ ERROR (Installments): $e");
return {"success": false, "message": e.toString()};
}
}
// -------------------------------------------------------
// ⭐ GET FULL INVOICE DETAILS (PRINT JSON HERE)
// -------------------------------------------------------
/// 🔵 NEW FUNCTION — Fetch Full Invoice Details
Future<Map<String, dynamic>> getInvoiceDetails(int invoiceId) async {
try {
final res = await dio.get("/user/invoice/$invoiceId/details");
print("👇👇👇 INVOICE API RESPONSE START 👇👇👇");
print(res.data); // <-- THIS IS WHAT YOU NEED
print("👆👆👆 INVOICE API RESPONSE END 👆👆👆");
return Map<String, dynamic>.from(res.data);
} catch (e) {
print("❌ ERROR (Invoice Details): $e");
return {"success": false, "message": e.toString()};
}
}

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

@@ -36,9 +36,13 @@ class ReverbSocketService {
_onAdminMessage = onAdminMessage; // 👈 SAVE
final uri = Uri.parse(
'ws://10.11.236.74:8080/app/q5fkk5rvcnatvbgadwvl'
'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");
@@ -60,6 +64,7 @@ class ReverbSocketService {
Future<void> _handleMessage(dynamic raw) async {
debugPrint("📥 RAW: $raw");
final payload = jsonDecode(raw);
final event = payload['event']?.toString() ?? '';
@@ -91,13 +96,15 @@ class ReverbSocketService {
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
// 🔁 Prevent duplicates by DB id
if (_receivedIds.contains(msgId)) {
debugPrint("🔁 DUPLICATE MESSAGE IGNORED: $msgId");
return;
@@ -106,20 +113,19 @@ class ReverbSocketService {
debugPrint("📩 NEW MESSAGE");
debugPrint("🆔 id=$msgId");
debugPrint("🧩 client_id=$incomingClientId");
debugPrint("👤 sender=$senderType");
debugPrint("💬 text=${data['message']}");
// Always push message to UI
// ✅ Forward FULL payload (with client_id) to UI
_onMessage(Map<String, dynamic>.from(data));
// 🔔 Increment unread ONLY if ADMIN sent message
// 🔔 Increment unread ONLY if ADMIN sent message
// 🔔 Unread count only for admin messages
if (senderType == 'App\\Models\\Admin') {
debugPrint("🔔 ADMIN MESSAGE → UNREAD +1");
_onAdminMessage(); // ✅ ACTUAL INCREMENT
_onAdminMessage();
}
return;
}
}

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

@@ -1,99 +1,118 @@
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;
// IMAGE PREVIEW
if (fileType.startsWith('image/')) {
return GestureDetector(
onTap: () => ChatFileViewer.open(
context,
url: url,
fileType: fileType,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
url,
width: 180,
height: 120,
fit: BoxFit.cover,
),
),
// LOCAL IMAGE (uploading preview)
if (isLocal && fileType.startsWith('image/')) {
return Image.file(
File(filePath),
width: 180,
height: 120,
fit: BoxFit.cover,
);
}
// VIDEO PREVIEW
if (fileType.startsWith('video/')) {
return GestureDetector(
onTap: () => ChatFileViewer.open(
context,
url: url,
fileType: fileType,
),
child: 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 / FILE PREVIEW
return GestureDetector(
onTap: () => ChatFileViewer.open(
context,
url: url,
fileType: fileType,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(_fileIcon(fileType), color: textColor),
const SizedBox(width: 8),
Text(
_fileLabel(fileType),
style: TextStyle(color: textColor),
),
],
),
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;

View File

@@ -16,7 +16,6 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
bool s1 = true;
bool s2 = false;
bool s3 = false;
bool s4 = false;
@override
void initState() {
@@ -27,8 +26,12 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
duration: const Duration(milliseconds: 350),
);
_slideAnimation = Tween(begin: const Offset(0, -0.1), end: Offset.zero)
.animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
_slideAnimation = Tween(
begin: const Offset(0, -0.1),
end: Offset.zero,
).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
);
_controller.forward();
}
@@ -40,13 +43,93 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
}
// ------------------------------------------------------------------
// HEADER SUMMARY CARD
// 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), // tighter
margin: EdgeInsets.only(bottom: 14 * scale), // closer
padding: EdgeInsets.all(16 * scale),
margin: EdgeInsets.only(bottom: 14 * scale),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade400, Colors.indigo.shade600],
@@ -76,40 +159,34 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
"Date: ${invoice['invoice_date']}",
style: TextStyle(color: Colors.white70, fontSize: 13 * scale),
),
SizedBox(height: 8 * scale),
Container(
padding: EdgeInsets.symmetric(
vertical: 5 * scale, horizontal: 12 * scale),
decoration: BoxDecoration(
color: Colors.white.withOpacity(.2),
borderRadius: BorderRadius.circular(40 * scale),
),
child: Text(
invoice['status'] ?? "Unknown",
style: TextStyle(
color: Colors.white,
fontSize: 13 * scale,
),
),
)
SizedBox(height: 10 * scale),
/// STATUS BADGE
statusBadge(invoice['status'] ?? 'Unknown', scale),
],
),
);
}
// ------------------------------------------------------------------
// SECTION HEADER (Closer Spacing)
// SECTION HEADER
// ------------------------------------------------------------------
Widget sectionHeader(
String title, IconData icon, bool expanded, Function() tap, double scale) {
String title,
IconData icon,
bool expanded,
VoidCallback tap,
double scale,
) {
return GestureDetector(
onTap: tap,
child: Container(
padding: EdgeInsets.all(12 * scale), // tighter
margin: EdgeInsets.only(bottom: 8 * scale), // closer
padding: EdgeInsets.all(12 * scale),
margin: EdgeInsets.only(bottom: 8 * scale),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.indigo.shade500, Colors.blue.shade400]),
colors: [Colors.indigo.shade500, Colors.blue.shade400],
),
borderRadius: BorderRadius.circular(12 * scale),
boxShadow: [
BoxShadow(
@@ -126,16 +203,20 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
Text(
title,
style: TextStyle(
fontSize: 15 * scale,
color: Colors.white,
fontWeight: FontWeight.bold),
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),
child: Icon(
Icons.keyboard_arrow_down,
color: Colors.white,
size: 20 * scale,
),
)
],
),
@@ -144,7 +225,7 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
}
// ------------------------------------------------------------------
// SECTION BODY (Closer Spacing)
// SECTION BODY
// ------------------------------------------------------------------
Widget sectionBody(bool visible, List<Widget> children, double scale) {
return AnimatedCrossFade(
@@ -155,17 +236,17 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
secondChild: SlideTransition(
position: _slideAnimation,
child: Container(
width: double.infinity,
padding: EdgeInsets.all(14 * scale), // tighter
margin: EdgeInsets.only(bottom: 10 * scale), // closer
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))
blurRadius: 7 * scale,
offset: Offset(0, 2 * scale),
color: Colors.black.withOpacity(.07),
)
],
),
child: Column(children: children),
@@ -175,11 +256,11 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
}
// ------------------------------------------------------------------
// DETAIL ROW (Closer Spacing)
// DETAIL ROW
// ------------------------------------------------------------------
Widget detailRow(IconData icon, String label, dynamic value, double scale) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 5 * scale), // closer
padding: EdgeInsets.symmetric(vertical: 5 * scale),
child: Row(
children: [
Icon(icon, size: 18 * scale, color: Colors.blueGrey),
@@ -196,11 +277,18 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
),
Expanded(
flex: 4,
child: Text(
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),
style: TextStyle(
fontSize: 14 * scale,
fontWeight: FontWeight.bold,
),
),
)
],
@@ -208,60 +296,6 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
);
}
// ------------------------------------------------------------------
// TIMELINE (Closer Spacing)
// ------------------------------------------------------------------
Widget invoiceTimeline(double scale) {
final steps = ["Invoice Created", "Payment Received", "Out for Delivery", "Completed"];
final icons = [Icons.receipt_long, Icons.payments, Icons.local_shipping, Icons.verified];
return Container(
padding: EdgeInsets.all(16 * scale),
margin: EdgeInsets.only(bottom: 16 * scale), // closer
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(14 * scale),
boxShadow: [
BoxShadow(
blurRadius: 7 * scale,
color: Colors.black.withOpacity(.1),
offset: Offset(0, 3 * scale),
)
],
),
child: Column(
children: List.generate(steps.length, (i) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4 * scale), // closer
child: Row(
children: [
Column(
children: [
Icon(icons[i], size: 22 * scale, color: Colors.indigo),
if (i < steps.length - 1)
Container(
height: 28 * scale,
width: 2 * scale,
color: Colors.indigo.shade300,
)
],
),
SizedBox(width: 10 * scale),
Expanded(
child: Text(
steps[i],
style:
TextStyle(fontSize: 14 * scale, fontWeight: FontWeight.w600),
),
)
],
),
);
}),
),
);
}
// ------------------------------------------------------------------
// MAIN BUILD
// ------------------------------------------------------------------
@@ -272,21 +306,30 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
final scale = (width / 390).clamp(0.85, 1.25);
return ListView(
padding: EdgeInsets.all(14 * scale), // slightly tighter
padding: EdgeInsets.all(14 * scale),
children: [
headerCard(invoice, scale),
invoiceTimeline(scale),
sectionHeader("Invoice Summary", Icons.receipt_long, s1,
() => setState(() => s1 = !s1), 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),
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),
@@ -294,21 +337,19 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
detailRow(Icons.location_on, "Address", invoice['customer_address'], scale),
], scale),
sectionHeader("Amount Details", Icons.currency_rupee, s3,
() => setState(() => s3 = !s3), 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),
sectionHeader("Payment Details", Icons.payment, s4,
() => setState(() => s4 = !s4), scale),
sectionBody(s4, [
detailRow(Icons.credit_card, "Method", invoice['payment_method'], scale),
detailRow(Icons.confirmation_number, "Reference",
invoice['reference_no'], scale),
], scale),
],
);
}

View File

@@ -544,6 +544,54 @@ packages:
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:

View File

@@ -49,7 +49,7 @@ dependencies:
video_player: ^2.9.1
chewie: ^1.8.1
flutter_pdfview: ^1.3.2
permission_handler: ^11.3.0

View File

@@ -7,12 +7,15 @@
#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(

View File

@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
permission_handler_windows
share_plus
url_launcher_windows
)