diff --git a/app/build.gradle b/app/build.gradle index 8580aa1..2b5270a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { applicationId "com.stock.pignon" minSdkVersion 17 targetSdkVersion 36 - versionCode 2 - versionName "0.2.0" + versionCode 3 + versionName "0.3.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/com/stock/pignon/CartActionHelper.java b/app/src/main/java/com/stock/pignon/CartActionHelper.java index a9b36a4..e606ea8 100644 --- a/app/src/main/java/com/stock/pignon/CartActionHelper.java +++ b/app/src/main/java/com/stock/pignon/CartActionHelper.java @@ -116,40 +116,47 @@ public class CartActionHelper { } /** - * Merges current cart items with the existing stock file on the SD Card. + * Merges current cart items with the existing stock file (json and csv) on the SD Card. */ private static void saveCartToExternalFile(List cartItems) { File dir = new File(Environment.getExternalStorageDirectory(), Config.EXTERNAL_DIR_NAME); - File stockFile = new File(dir, Config.STOCK_FILE_NAME); + File stockFile = new File(dir, Config.OUPUT_JSON_NAME); + File csvFile = new File(dir, Config.OUPUT_CSV_NAME); Gson gson = new Gson(); - Map stockMap; + // Load previous list + Map stockMap = loadExistingStock(stockFile, gson); - // Load data into a Map (Key: Item Name, Value: Total Quantity) - stockMap = loadExistingStock(stockFile, gson); - - // Merge with new items from current cart + // Add current cart item for (CartItem item : cartItems) { - // Get previous quantity Integer qtyObj = stockMap.get(item.getName()); int currentQty = (qtyObj != null) ? qtyObj : 0; - // Add stockMap.put(item.getName(), currentQty + item.getQuantity()); } - // Overwrite the file with updated data - // Needs WRITE_EXTERNAL_STORAGE permission + // Save to JSON try (FileOutputStream fos = new FileOutputStream(stockFile); - @SuppressWarnings("CharsetObjectCanBeUsed") OutputStreamWriter writer = new OutputStreamWriter(fos, "UTF-8")) { - - // Writing directly to the stream gson.toJson(stockMap, writer); - Log.i(TAG, "Stock updated successfully at: " + stockFile.getAbsolutePath()); - } catch (Exception e) { Log.e(TAG, "Failed to write stock file", e); } + + // Save to CSV, french format with ";" + try (FileOutputStream fos = new FileOutputStream(csvFile); + OutputStreamWriter writer = new OutputStreamWriter(fos, "UTF-8")) { + + // UTF-8 BOM and columns headers + writer.write('\ufeff'); + writer.write("Article;Quantité\n"); + + for (Map.Entry entry : stockMap.entrySet()) { + writer.write(entry.getKey().replace(";", ",") + ";" + entry.getValue() + "\n"); + } + Log.i(TAG, "CSV Export updated successfully"); + } catch (Exception e) { + Log.e(TAG, "Failed to write CSV file", e); + } } /** diff --git a/app/src/main/java/com/stock/pignon/Config.java b/app/src/main/java/com/stock/pignon/Config.java index c4ed597..919ed66 100644 --- a/app/src/main/java/com/stock/pignon/Config.java +++ b/app/src/main/java/com/stock/pignon/Config.java @@ -9,8 +9,11 @@ public class Config { public static final String IMAGES_SUBDIR_NAME = "images"; // Input json - public static final String PIECES_FILE_NAME = "pieces.json"; + public static final String INPUT_JSON_NAME = "pieces.json"; // Output json - public static final String STOCK_FILE_NAME = "stock.json"; + public static final String OUPUT_JSON_NAME = "stock.json"; + + // Output json + public static final String OUPUT_CSV_NAME = "stock.csv"; } \ No newline at end of file diff --git a/app/src/main/java/com/stock/pignon/ControlServer.java b/app/src/main/java/com/stock/pignon/ControlServer.java index def4ca4..8644783 100644 --- a/app/src/main/java/com/stock/pignon/ControlServer.java +++ b/app/src/main/java/com/stock/pignon/ControlServer.java @@ -37,12 +37,25 @@ public class ControlServer extends NanoHTTPD { // Get requested address String uri = session.getUri(); - // Adapt URL to find the file : /stock.json become stock.json - if (uri.equals("/download_orders")) { - return downloadFile(Config.STOCK_FILE_NAME); - } - if (uri.equals("/download_json")) { - return downloadFile(Config.PIECES_FILE_NAME); + switch (uri) { + case "/download_input": + return downloadFile(Config.INPUT_JSON_NAME); + + case "/output_json": + return viewFile(Config.OUPUT_JSON_NAME); + + case "/download_output_json": + return downloadFile(Config.OUPUT_JSON_NAME); + + case "/output_csv": + return viewFile(Config.OUPUT_CSV_NAME); + + case "/download_output_csv": + return downloadFile(Config.OUPUT_CSV_NAME); + + case "/": + case "/index.html": + return newFixedLengthResponse(getHtmlResponse()); } // File upload management @@ -54,8 +67,7 @@ public class ControlServer extends NanoHTTPD { } } - // Return UI - return newFixedLengthResponse(getHtmlResponse()); + return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "❌ Page non trouvée"); } /** @@ -71,7 +83,7 @@ public class ControlServer extends NanoHTTPD { "h1 { color: #0049AF; border-bottom: 2px solid #0049AF; }" + "h2 { color: #555; margin-top: 30px; }" + ".card { background: #f4f4f4; padding: 15px; border-radius: 8px; margin-bottom: 20px; border: 1px solid #ddd; }" + - ".btn { display: inline-block; width: 280px; height: 45px; line-height: 45px; text-align: center; background: #0049AF; color: white; text-decoration: none; border-radius: 4px; border: none; cursor: pointer; font-weight: bold; box-sizing: border-box; padding: 0; -webkit-appearance: none; font-family: inherit; font-size: 14px; font-style: normal; letter-spacing: normal; }" + + ".btn { display: inline-block; width: 280px; height: 45px; line-height: 45px; text-align: center; background: #0049AF; color: white; text-decoration: none; border-radius: 4px; border: none; cursor: pointer; font-weight: bold; box-sizing: border-box; padding: 0; -webkit-appearance: none; font-family: inherit; font-size: 14px; font-style: normal; letter-spacing: normal; margin: 10px 5px; vertical-align: middle;}" + ".btn-download { background: #2E7D32; }" + "input[type='file'] { margin: 10px 0; }" + ".status { font-weight: bold; color: green; }" + @@ -84,13 +96,16 @@ public class ControlServer extends NanoHTTPD { "
" + "

Récupérer les sacoches des adhérent·es

" + "

Télécharger le décompte des pièces sorties de l'atelier.

" + - "Télécharger stock.json" + + "Voir stock.json" + + "Voir stock.csv" + + "Télécharger stock.json" + + "Télécharger stock.csv" + "
" + "

Modifier le catalogue

" + "
" + "

Gestion du fichier JSON

" + - "

Catalogue actuel : Télécharger pieces.json

" + + "

Catalogue actuel : Télécharger pieces.json

" + "
" + "
" + "

Envoyer un nouveau catalogue :

" + @@ -124,7 +139,7 @@ public class ControlServer extends NanoHTTPD { if (tmpPath == null) return newFixedLengthResponse("❌ Aucun fichier reçu."); File src = new File(tmpPath); - File dest = new File(baseDir, Config.PIECES_FILE_NAME); + File dest = new File(baseDir, Config.INPUT_JSON_NAME); copyFile(src, dest); return newFixedLengthResponse("✅ Catalogue mis à jour ! Retour"); @@ -179,20 +194,43 @@ public class ControlServer extends NanoHTTPD { } /** - * Prepare local file to send it to remote. + * Prepare file and offer as download */ private Response downloadFile(String filename) { File file = new File(baseDir, filename); if (!file.exists()) { - return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "❌ Fichier non trouvé : " + filename); + return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "❌ Fichier non trouvé."); } try { InputStream is = new FileInputStream(file); + + // Force download Response res = newChunkedResponse(Response.Status.OK, "application/octet-stream", is); - // Force file download name res.addHeader("Content-Disposition", "attachment; filename=\"" + filename + "\""); + return res; + } catch (IOException e) { + return newFixedLengthResponse("❌ Erreur de lecture."); + } + } + /** + * Prepare file and print it in browser + */ + private Response viewFile(String filename) { + File file = new File(baseDir, filename); + if (!file.exists()) { + return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "❌ Fichier non trouvé."); + } + try { + InputStream is = new FileInputStream(file); + + // Force Mime type to print file inside browser + String mimeType = "text/plain"; + + Response res = newChunkedResponse(Response.Status.OK, mimeType + "; charset=utf-8", is); + + res.addHeader("Content-Disposition", "inline"); return res; } catch (IOException e) { return newFixedLengthResponse("❌ Erreur de lecture."); diff --git a/app/src/main/java/com/stock/pignon/DataLoader.java b/app/src/main/java/com/stock/pignon/DataLoader.java index 289ad94..4206841 100644 --- a/app/src/main/java/com/stock/pignon/DataLoader.java +++ b/app/src/main/java/com/stock/pignon/DataLoader.java @@ -13,7 +13,7 @@ import java.util.List; public class DataLoader { private static final String TAG = "DataLoader"; // For readable logs private static final String EXTERNAL_DIR = Config.EXTERNAL_DIR_NAME; - private static final String PIECES_FILE = Config.PIECES_FILE_NAME; + private static final String PIECES_FILE = Config.INPUT_JSON_NAME; // Raw data private static List cachedCategories = new ArrayList<>(); diff --git a/app/src/main/java/com/stock/pignon/MainActivity.java b/app/src/main/java/com/stock/pignon/MainActivity.java index d79fa8e..e62fd60 100644 --- a/app/src/main/java/com/stock/pignon/MainActivity.java +++ b/app/src/main/java/com/stock/pignon/MainActivity.java @@ -290,7 +290,7 @@ public class MainActivity extends AppCompatActivity { // Second safety : are we able to create folder ? if (folder.mkdirs()) { // Copy JSON file - copyFileFromAssets(Config.PIECES_FILE_NAME, new File(folder, Config.PIECES_FILE_NAME)); + copyFileFromAssets(Config.INPUT_JSON_NAME, new File(folder, Config.INPUT_JSON_NAME)); // Copy images subfolder File imgFolder = new File(folder, Config.IMAGES_SUBDIR_NAME);