update invoice section show download option

This commit is contained in:
Abhishek Mali
2025-12-18 12:45:26 +05:30
parent b9fb9455e7
commit d606156a6d
12 changed files with 179 additions and 121 deletions

File diff suppressed because one or more lines are too long

View File

@@ -6,8 +6,11 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" /> android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<uses-permission android:name="android.permission.READ_MEDIA_DOCUMENTS" />
<application <application

View File

@@ -1,3 +1,4 @@
class ApiConfig { class ApiConfig {
static const String baseUrl = "http://10.119.0.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

@@ -176,17 +176,17 @@ class InstallmentCard extends StatelessWidget {
Divider(color: Colors.grey.shade300, thickness: 1), Divider(color: Colors.grey.shade300, thickness: 1),
SizedBox(height: isTablet ? 10 : 6), SizedBox(height: isTablet ? 10 : 6),
Align( // Align(
alignment: Alignment.centerRight, // alignment: Alignment.centerRight,
child: Text( // child: Text(
"Installment #${inst['id'] ?? ''}", // "Installment #${inst['id'] ?? ''}",
style: TextStyle( // style: TextStyle(
fontSize: isTablet ? 15 : 13, // fontSize: isTablet ? 15 : 13,
color: Colors.grey.shade600, // color: Colors.grey.shade600,
fontWeight: FontWeight.w500, // fontWeight: FontWeight.w500,
), // ),
), // ),
), // ),
], ],
), ),
), ),

View File

@@ -1,9 +1,12 @@
import 'dart:io'; 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/dio_client.dart';
import '../services/invoice_service.dart'; import '../services/invoice_service.dart';
import '../widgets/invoice_detail_view.dart'; import '../widgets/invoice_detail_view.dart';
@@ -26,9 +29,6 @@ class _InvoiceDetailScreenState extends State<InvoiceDetailScreen> {
load(); load();
} }
// -------------------------------------------------------
// ⭐ LOAD INVOICE FROM API
// -------------------------------------------------------
Future<void> load() async { Future<void> load() async {
final service = InvoiceService(DioClient.getInstance(context)); final service = InvoiceService(DioClient.getInstance(context));
try { try {
@@ -40,93 +40,116 @@ class _InvoiceDetailScreenState extends State<InvoiceDetailScreen> {
invoice = {}; invoice = {};
} }
} catch (e) { } catch (e) {
// handle error gracefully
invoice = {}; invoice = {};
} finally { } finally {
if (mounted) setState(() => loading = false); if (mounted) setState(() => loading = false);
} }
} }
// ------------------------------------------------------- String? get pdfUrl {
// ⭐ GENERATE + SAVE PDF TO DOWNLOADS FOLDER final path = invoice['pdf_path'];
// (No Permission Needed) if (path == null || path.toString().isEmpty) return null;
// -------------------------------------------------------
Future<File> generatePDF() async {
final pdf = pw.Document();
pdf.addPage( return ApiConfig.fileBaseUrl + path;
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'] ?? '-'}"), static const MethodChannel _mediaScanner =
pw.Text("Amount: ₹${invoice['amount'] ?? '-'}"), MethodChannel('media_scanner');
pw.Text("Status: ${invoice['status'] ?? '-'}"),
pw.Text("Date: ${invoice['date'] ?? '-'}"),
pw.Text("Customer: ${invoice['customer_name'] ?? '-'}"),
],
),
),
);
// ⭐ SAFEST WAY (Android 1014) Future<void> _scanFile(String path) async {
final downloadsDir = await getDownloadsDirectory(); try {
await _mediaScanner.invokeMethod('scanFile', {'path': path});
final filePath = "${downloadsDir!.path}/invoice_${invoice['id']}.pdf"; } catch (e) {
final file = File(filePath); debugPrint("❌ MediaScanner error: $e");
await file.writeAsBytes(await pdf.save());
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("PDF saved to Downloads:\n$filePath")),
);
} }
return file;
} }
// -------------------------------------------------------
// ⭐ SHARE THE SAVED PDF FILE
// -------------------------------------------------------
Future<void> sharePDF() async {
final file = await generatePDF();
await Share.shareXFiles(
[XFile(file.path)], Future<File?> _downloadPdf(String url) async {
text: "Invoice Details", try {
); debugPrint("📥 PDF URL: $url");
final fileName = url.split('/').last;
// ✅ SAME FOLDER AS CHAT FILES
final baseDir = Directory('/storage/emulated/0/Download/KentChat');
if (!await baseDir.exists()) {
await baseDir.create(recursive: true);
debugPrint("📁 Created directory: ${baseDir.path}");
}
final filePath = "${baseDir.path}/$fileName";
debugPrint("📄 Saving PDF to: $filePath");
await Dio().download(
url,
filePath,
onReceiveProgress: (received, total) {
if (total > 0) {
debugPrint(
"⬇ Downloading: ${(received / total * 100).toStringAsFixed(1)}%",
);
}
},
);
// 🔔 VERY IMPORTANT: Notify Android system (same as chat)
await _scanFile(filePath);
debugPrint("✅ PDF Downloaded & Scanned Successfully");
return File(filePath);
} catch (e) {
debugPrint("❌ PDF download error: $e");
return null;
}
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width; final width = MediaQuery.of(context).size.width;
// ⭐ COMPACT RESPONSIVE SCALE
final scale = (width / 430).clamp(0.88, 1.08); final scale = (width / 430).clamp(0.88, 1.08);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text( title: const Text("Invoice Details"),
"Invoice Details", actions: pdfUrl == null
style: TextStyle( ? []
fontSize: 18 * scale, : [
fontWeight: FontWeight.w600, // 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 // SHARE
actions: [
IconButton( IconButton(
icon: const Icon(Icons.picture_as_pdf), icon: const Icon(Icons.share_rounded),
onPressed: generatePDF, onPressed: () async {
), final file = await _downloadPdf(pdfUrl!);
IconButton( if (file != null) {
icon: const Icon(Icons.share), await Share.shareXFiles(
onPressed: sharePDF, [XFile(file.path)],
text: "Invoice ${invoice['invoice_number']}",
);
}
},
), ),
], ],
), ),
@@ -146,6 +169,8 @@ class _InvoiceDetailScreenState extends State<InvoiceDetailScreen> {
) )
: Padding( : Padding(
padding: EdgeInsets.all(12 * scale), 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), child: InvoiceDetailView(invoice: invoice),
), ),
); );

View File

@@ -92,9 +92,9 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
_detailRow(Icons.numbers, "Invoice No", invoice['invoice_number'], scale), _detailRow(Icons.numbers, "Invoice No", invoice['invoice_number'], scale),
_detailRow(Icons.calendar_month, "Invoice Date", invoice['invoice_date'], scale), _detailRow(Icons.calendar_month, "Invoice Date", invoice['invoice_date'], scale),
_detailRow(Icons.date_range, "Due Date", invoice['due_date'], scale), _detailRow(Icons.date_range, "Due Date", invoice['due_date'], scale),
_detailRow(Icons.payment, "Payment Method", invoice['payment_method'], scale), // _detailRow(Icons.payment, "Payment Method", invoice['payment_method'], scale),
_detailRow(Icons.confirmation_number, "Reference No", // _detailRow(Icons.confirmation_number, "Reference No",
invoice['reference_no'], scale), // invoice['reference_no'], scale),
], scale), ], scale),
// AMOUNT SECTION // AMOUNT SECTION
@@ -103,6 +103,7 @@ class _OrderInvoiceScreenState extends State<OrderInvoiceScreen>
}, scale), }, scale),
_sectionBody(s2, [ _sectionBody(s2, [
_detailRow(Icons.money, "Amount", invoice['final_amount'], scale), _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.percent, "GST Amount", invoice['gst_amount'], scale),
_detailRow(Icons.summarize, "Final With GST", _detailRow(Icons.summarize, "Final With GST",
invoice['final_amount_with_gst'], scale), invoice['final_amount_with_gst'], scale),

View File

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

View File

@@ -298,17 +298,18 @@ class _InvoiceDetailViewState extends State<InvoiceDetailView>
() => setState(() => s3 = !s3), scale), () => setState(() => s3 = !s3), scale),
sectionBody(s3, [ sectionBody(s3, [
detailRow(Icons.money, "Amount", invoice['final_amount'], scale), 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), detailRow(Icons.summarize, "Total", invoice['final_amount_with_gst'], scale),
], scale), ], scale),
sectionHeader("Payment Details", Icons.payment, s4, // sectionHeader("Payment Details", Icons.payment, s4,
() => setState(() => s4 = !s4), scale), // () => setState(() => s4 = !s4), scale),
sectionBody(s4, [ // sectionBody(s4, [
detailRow(Icons.credit_card, "Method", invoice['payment_method'], scale), // detailRow(Icons.credit_card, "Method", invoice['payment_method'], scale),
detailRow(Icons.confirmation_number, "Reference", // detailRow(Icons.confirmation_number, "Reference",
invoice['reference_no'], scale), // invoice['reference_no'], scale),
], scale), // ], scale),
], ],
); );
} }

View File

@@ -544,6 +544,54 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.11.3" 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: petitparser:
dependency: transitive dependency: transitive
description: description:

View File

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

View File

@@ -7,12 +7,15 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <file_selector_windows/file_selector_windows.h> #include <file_selector_windows/file_selector_windows.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h> #include <share_plus/share_plus_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar( FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows")); registry->GetRegistrarForPlugin("FileSelectorWindows"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar( SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(

View File

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