From 9e090c465a32edb28fbfdf80fa44e734a29c03d3 Mon Sep 17 00:00:00 2001 From: Horacio Daniel Ros Date: Fri, 14 Aug 2026 19:51:59 -0300 Subject: [PATCH] feat: implementar descarga de PDF de factura y soporte para guardar en Android --- .../hdrdevs/turnosxpress/MainActivity.java | 69 ++++++++++++++++--- .../org/profile/[id]/cash/movements/page.tsx | 11 ++- 2 files changed, 68 insertions(+), 12 deletions(-) diff --git a/com.hdrdevs.turnosxpress/app/src/main/java/com/hdrdevs/turnosxpress/MainActivity.java b/com.hdrdevs.turnosxpress/app/src/main/java/com/hdrdevs/turnosxpress/MainActivity.java index 585b10b..d7f8bfb 100644 --- a/com.hdrdevs.turnosxpress/app/src/main/java/com/hdrdevs/turnosxpress/MainActivity.java +++ b/com.hdrdevs.turnosxpress/app/src/main/java/com/hdrdevs/turnosxpress/MainActivity.java @@ -1,11 +1,17 @@ package com.hdrdevs.turnosxpress; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import android.Manifest; import android.os.Bundle; +import android.os.Build; import android.os.Message; import android.content.ActivityNotFoundException; import android.content.Intent; +import android.content.pm.PackageManager; +import android.media.MediaScannerConnection; import android.net.Uri; import android.util.Log; import android.view.KeyEvent; @@ -33,6 +39,7 @@ import android.content.ContentResolver; public class MainActivity extends AppCompatActivity { private static final int LOGIN_REQUEST_CODE = 1; + private static final int STORAGE_PERMISSION_REQUEST_CODE = 2; private WebView webView; private ValueCallback fileChooserCallback; @@ -43,6 +50,7 @@ public class MainActivity extends AppCompatActivity { super.onCreate(savedInstanceState); tokenResult = ""; + requestLegacyStoragePermissionIfNeeded(); // For Android Emulator, use 10.0.2.2 to access localhost on your development machine. // If testing on a physical device, replace 10.0.2.2 with your development machine's actual local IP address. @@ -220,6 +228,28 @@ public class MainActivity extends AppCompatActivity { } } + private void requestLegacyStoragePermissionIfNeeded() { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P + && ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, STORAGE_PERMISSION_REQUEST_CODE); + } + } + + private String safePdfFilename(String filename) { + String safeName = filename == null ? "reporte.pdf" : filename.trim(); + safeName = safeName.replaceAll("[\\\\/:*?\"<>|]", "_"); + + if (safeName.length() == 0) { + safeName = "reporte.pdf"; + } + + if (!safeName.toLowerCase().endsWith(".pdf")) { + safeName += ".pdf"; + } + + return safeName; + } + public class WebAppInterface { @JavascriptInterface public void startGoogleLogin() { @@ -235,16 +265,20 @@ public class MainActivity extends AppCompatActivity { @JavascriptInterface public void savePdf(String base64Data, String filename) { + String safeFilename = safePdfFilename(filename); + try { - byte[] pdfAsBytes = Base64.decode(base64Data, 0); + String normalizedBase64 = base64Data == null ? "" : base64Data.replaceFirst("^data:application/pdf;base64,", ""); + byte[] pdfAsBytes = Base64.decode(normalizedBase64, Base64.DEFAULT); boolean isSaved = false; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { ContentResolver resolver = getContentResolver(); ContentValues contentValues = new ContentValues(); - contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, filename); + contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, safeFilename); contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "application/pdf"); contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS); + contentValues.put(MediaStore.MediaColumns.IS_PENDING, 1); Uri uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues); @@ -253,18 +287,31 @@ public class MainActivity extends AppCompatActivity { outputStream.write(pdfAsBytes); isSaved = true; } catch (IOException e) { - e.printStackTrace(); + Log.e("WEBVIEW", "Error writing PDF to MediaStore: " + safeFilename, e); + resolver.delete(uri, null, null); + } + + if (isSaved) { + ContentValues completedValues = new ContentValues(); + completedValues.put(MediaStore.MediaColumns.IS_PENDING, 0); + resolver.update(uri, completedValues, null, null); } } } else { File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); - File file = new File(path, filename); - try (FileOutputStream os = new FileOutputStream(file)) { - os.write(pdfAsBytes); - isSaved = true; - } catch (IOException e) { - e.printStackTrace(); + if (!path.exists() && !path.mkdirs()) { + Log.e("WEBVIEW", "Could not create Downloads directory: " + path.getAbsolutePath()); + } else { + File file = new File(path, safeFilename); + + try (FileOutputStream os = new FileOutputStream(file)) { + os.write(pdfAsBytes); + isSaved = true; + MediaScannerConnection.scanFile(MainActivity.this, new String[]{file.getAbsolutePath()}, new String[]{"application/pdf"}, null); + } catch (IOException e) { + Log.e("WEBVIEW", "Error writing PDF to Downloads: " + safeFilename, e); + } } } @@ -275,7 +322,7 @@ public class MainActivity extends AppCompatActivity { } } catch (Exception e) { - e.printStackTrace(); + Log.e("WEBVIEW", "Error saving PDF: " + safeFilename, e); runOnUiThread(() -> Toast.makeText(MainActivity.this, "Error: " + e.getMessage(), Toast.LENGTH_LONG).show()); } } diff --git a/txclient/src/app/admin/(organization-profile)/org/profile/[id]/cash/movements/page.tsx b/txclient/src/app/admin/(organization-profile)/org/profile/[id]/cash/movements/page.tsx index fda58ac..3c18727 100644 --- a/txclient/src/app/admin/(organization-profile)/org/profile/[id]/cash/movements/page.tsx +++ b/txclient/src/app/admin/(organization-profile)/org/profile/[id]/cash/movements/page.tsx @@ -844,7 +844,16 @@ export default function CashFlowMovementsList() { }, }; - (pdfMake as any).createPdf({ ...documentDefinition, content }).download(`factura-c-${invoice.pointOfSale || fiscalProfile.pointOfSale || 0}-${invoice.voucherNumber || 0}.pdf`); + const filename = `factura-c-${invoice.pointOfSale || fiscalProfile.pointOfSale || 0}-${invoice.voucherNumber || 0}.pdf`; + const pdf = (pdfMake as any).createPdf({ ...documentDefinition, content }); + + if ((window as unknown as { Android: any }).Android && (window as unknown as { Android: any }).Android.savePdf) { + pdf.getBase64((data: string) => { + (window as unknown as { Android: any }).Android?.savePdf(data, filename); + }); + } else { + pdf.download(filename); + } } catch (error) { console.error("Error generating invoice PDF:", error); alert.showError("No pudimos generar el PDF de la factura.");