Compare commits
2 Commits
bbde34fae4
...
d606156a6d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d606156a6d | ||
|
|
b9fb9455e7 |
File diff suppressed because one or more lines are too long
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
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/";
|
||||
}
|
||||
|
||||
@@ -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://10.119.0.74:8000/images/kent_logo2.png";
|
||||
|
||||
// Which one to use?
|
||||
static const String logoUrl = logoUrlDevice; // CHANGE THIS WHEN TESTING ON REAL DEVICE
|
||||
|
||||
@@ -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,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -24,6 +24,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,18 +49,54 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
|
||||
_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? file = await openFile();
|
||||
final XFile? picked = await openFile();
|
||||
if (picked == null || ticketId == null) return;
|
||||
|
||||
if (file == null || ticketId == null) return;
|
||||
final file = File(picked.path);
|
||||
|
||||
final dartFile = File(file.path);
|
||||
// 1️⃣ Show uploading UI
|
||||
setState(() {
|
||||
uploadingMessage = {
|
||||
'local_file': file,
|
||||
'file_type': _guessMimeType(file.path),
|
||||
'progress': 0.0,
|
||||
};
|
||||
});
|
||||
|
||||
await _chatService.sendFile(ticketId!, dartFile);
|
||||
// 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 +114,23 @@ 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();
|
||||
@@ -112,16 +162,35 @@ 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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// ============================
|
||||
// DISPOSE
|
||||
// ============================
|
||||
@@ -226,18 +295,14 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
|
||||
|
||||
Widget _buildMessages() {
|
||||
return ListView.builder(
|
||||
return ListView(
|
||||
controller: _scrollCtrl,
|
||||
padding: const EdgeInsets.all(12),
|
||||
itemCount: messages.length,
|
||||
itemBuilder: (_, index) {
|
||||
final msg = messages[index];
|
||||
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(
|
||||
@@ -247,26 +312,66 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
color: isUser ? Colors.blue : Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
|
||||
// 🔽 ONLY THIS PART CHANGED
|
||||
child: filePath == null
|
||||
child: msg['file_path'] == null
|
||||
? Text(
|
||||
message ?? '',
|
||||
msg['message'] ?? '',
|
||||
style: TextStyle(
|
||||
color: isUser ? Colors.white : Colors.black,
|
||||
),
|
||||
)
|
||||
: ChatFilePreview(
|
||||
filePath: filePath,
|
||||
fileType: fileType ?? '',
|
||||
filePath: msg['file_path'],
|
||||
fileType: msg['file_type'] ?? '',
|
||||
isUser: isUser,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
}),
|
||||
|
||||
// ⏳ UPLOADING MESSAGE
|
||||
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: [
|
||||
ChatFilePreview(
|
||||
filePath: uploadingMessage!['local_file'].path,
|
||||
fileType: uploadingMessage!['file_type'],
|
||||
isUser: true,
|
||||
isLocal: true,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
LinearProgressIndicator(
|
||||
value: uploadingMessage!['progress'],
|
||||
backgroundColor: Colors.white24,
|
||||
valueColor:
|
||||
const AlwaysStoppedAnimation(Colors.white),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
"Sending…",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildInput() {
|
||||
return SafeArea(
|
||||
child: Row(
|
||||
|
||||
@@ -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),
|
||||
|
||||
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'] ?? '-'}"),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// ⭐ SAFEST WAY (Android 10–14)
|
||||
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")),
|
||||
);
|
||||
return ApiConfig.fileBaseUrl + path;
|
||||
}
|
||||
|
||||
return file;
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// ⭐ 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,6 +169,8 @@ 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),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -92,9 +92,9 @@ 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),
|
||||
// _detailRow(Icons.payment, "Payment Method", invoice['payment_method'], scale),
|
||||
// _detailRow(Icons.confirmation_number, "Reference No",
|
||||
// invoice['reference_no'], scale),
|
||||
], scale),
|
||||
|
||||
// AMOUNT SECTION
|
||||
@@ -103,6 +103,7 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
|
||||
}, 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),
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import 'token_interceptor.dart';
|
||||
class DioClient {
|
||||
static Dio? _dio;
|
||||
|
||||
static const String baseUrl = "http://10.11.236.74:8000";
|
||||
static const String baseUrl = "http://10.119.0.74:8000";
|
||||
|
||||
static Dio getInstance(BuildContext context) {
|
||||
if (_dio == null) {
|
||||
|
||||
@@ -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()};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ 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',
|
||||
);
|
||||
|
||||
@@ -60,6 +60,7 @@ class ReverbSocketService {
|
||||
Future<void> _handleMessage(dynamic raw) async {
|
||||
debugPrint("📥 RAW: $raw");
|
||||
|
||||
|
||||
final payload = jsonDecode(raw);
|
||||
final event = payload['event']?.toString() ?? '';
|
||||
|
||||
@@ -91,13 +92,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 +109,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;
|
||||
}
|
||||
}
|
||||
|
||||
118
lib/widgets/chat_file_actions.dart
Normal file
118
lib/widgets/chat_file_actions.dart
Normal file
@@ -0,0 +1,118 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import '../screens/chat_file_viewer.dart';
|
||||
|
||||
class ChatFileActions {
|
||||
static void show(
|
||||
BuildContext context, {
|
||||
required String url,
|
||||
required String fileType,
|
||||
}) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (_) {
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_actionTile(
|
||||
icon: Icons.open_in_new,
|
||||
title: "Open",
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
ChatFileViewer.open(
|
||||
context,
|
||||
url: url,
|
||||
fileType: fileType,
|
||||
);
|
||||
},
|
||||
),
|
||||
_actionTile(
|
||||
icon: Icons.download,
|
||||
title: "Download",
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
await _downloadFile(context, url, fileType);
|
||||
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _actionTile({
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return ListTile(
|
||||
leading: Icon(icon),
|
||||
title: Text(title),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
static Future<void> _scanFile(String path) async {
|
||||
const platform = MethodChannel('media_scanner');
|
||||
|
||||
try {
|
||||
await platform.invokeMethod('scanFile', {'path': path});
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
|
||||
// ===========================
|
||||
// DOWNLOAD LOGIC
|
||||
// ===========================
|
||||
static Future<void> _downloadFile(
|
||||
BuildContext context,
|
||||
String url,
|
||||
String fileType,
|
||||
) async {
|
||||
try {
|
||||
final fileName = url.split('/').last;
|
||||
|
||||
Directory baseDir;
|
||||
|
||||
if (fileType.startsWith('image/')) {
|
||||
baseDir = Directory('/storage/emulated/0/Pictures/KentChat');
|
||||
} else if (fileType.startsWith('video/')) {
|
||||
baseDir = Directory('/storage/emulated/0/Movies/KentChat');
|
||||
} else {
|
||||
baseDir = Directory('/storage/emulated/0/Download/KentChat');
|
||||
}
|
||||
|
||||
if (!await baseDir.exists()) {
|
||||
await baseDir.create(recursive: true);
|
||||
}
|
||||
|
||||
final filePath = "${baseDir.path}/$fileName";
|
||||
|
||||
await Dio().download(url, filePath);
|
||||
|
||||
// 🔔 IMPORTANT: Tell Android to scan the file
|
||||
await _scanFile(filePath);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Saved to ${baseDir.path}")),
|
||||
);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("Download failed")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,33 +1,66 @@
|
||||
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/')) {
|
||||
// LOCAL IMAGE (uploading preview)
|
||||
if (isLocal && fileType.startsWith('image/')) {
|
||||
return Image.file(
|
||||
File(filePath),
|
||||
width: 180,
|
||||
height: 120,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => ChatFileViewer.open(
|
||||
onTap: () {
|
||||
// ✅ TAP = OPEN
|
||||
ChatFileViewer.open(
|
||||
context,
|
||||
url: url,
|
||||
fileType: fileType,
|
||||
),
|
||||
child: ClipRRect(
|
||||
);
|
||||
},
|
||||
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,
|
||||
@@ -35,19 +68,12 @@ class ChatFilePreview extends StatelessWidget {
|
||||
height: 120,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// VIDEO PREVIEW
|
||||
// VIDEO
|
||||
if (fileType.startsWith('video/')) {
|
||||
return GestureDetector(
|
||||
onTap: () => ChatFileViewer.open(
|
||||
context,
|
||||
url: url,
|
||||
fileType: fileType,
|
||||
),
|
||||
child: Stack(
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Container(
|
||||
@@ -69,18 +95,11 @@ class ChatFilePreview extends StatelessWidget {
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// PDF / FILE PREVIEW
|
||||
return GestureDetector(
|
||||
onTap: () => ChatFileViewer.open(
|
||||
context,
|
||||
url: url,
|
||||
fileType: fileType,
|
||||
),
|
||||
child: Row(
|
||||
// PDF / OTHER FILES
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(_fileIcon(fileType), color: textColor),
|
||||
@@ -90,10 +109,10 @@ class ChatFilePreview extends StatelessWidget {
|
||||
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;
|
||||
|
||||
@@ -298,17 +298,18 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
|
||||
() => 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 percent %", 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),
|
||||
// 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),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
48
pubspec.lock
48
pubspec.lock
@@ -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:
|
||||
|
||||
@@ -49,7 +49,7 @@ dependencies:
|
||||
video_player: ^2.9.1
|
||||
chewie: ^1.8.1
|
||||
flutter_pdfview: ^1.3.2
|
||||
|
||||
permission_handler: ^11.3.0
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_windows
|
||||
permission_handler_windows
|
||||
share_plus
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user