13 Commits

88 changed files with 1402 additions and 291 deletions

View File

@@ -2,6 +2,8 @@
Application Android (4.2+) de gestion de stock et d'aide à la vente à prix libre. Conçue pour l'[**Atelier du Pignon**](https://www.atelierdupignon.fr/), atelier d'auto-réparation à Nantes. Application Android (4.2+) de gestion de stock et d'aide à la vente à prix libre. Conçue pour l'[**Atelier du Pignon**](https://www.atelierdupignon.fr/), atelier d'auto-réparation à Nantes.
![Pignon1](https://github.com/user-attachments/assets/1e43e8f2-872c-41c2-9e12-405e41db5ca1)
## 🛠 Fonctionnalités ## 🛠 Fonctionnalités
- **Catalogue visuel** : Permet aux adhérent·es de noter rapidement les pièces qu'iels ont emportées. - **Catalogue visuel** : Permet aux adhérent·es de noter rapidement les pièces qu'iels ont emportées.
- **Prix libre & conscient** : Calculateur de panier affichant une fourchette de prix suggérée. - **Prix libre & conscient** : Calculateur de panier affichant une fourchette de prix suggérée.
@@ -29,3 +31,9 @@ Application Android (4.2+) de gestion de stock et d'aide à la vente à prix lib
Ce projet est sous licence **CC BY-NC-SA 4.0**. Ce projet est sous licence **CC BY-NC-SA 4.0**.
Cela signifie que vous êtes libre de partager et d'adapter le code, tant que vous citez l'auteur original, que vous n'en faites pas un usage commercial, et que vous diffusez vos modifications sous la même licence. Cela signifie que vous êtes libre de partager et d'adapter le code, tant que vous citez l'auteur original, que vous n'en faites pas un usage commercial, et que vous diffusez vos modifications sous la même licence.
## 👀 Screenshots
![Pignon2](https://github.com/user-attachments/assets/78439dff-cde4-4d75-8a8f-f6ee38a69abf)
![Pignon3](https://github.com/user-attachments/assets/51694675-ec5f-4a59-95a9-7b7e0dd1caf3)
![Pignon4](https://github.com/user-attachments/assets/f402250c-045c-4b3d-bbdc-11ae22bd62b2)

View File

@@ -10,8 +10,8 @@ android {
applicationId "com.stock.pignon" applicationId "com.stock.pignon"
minSdkVersion 17 minSdkVersion 17
targetSdkVersion 36 targetSdkVersion 36
versionCode 1 versionCode 8
versionName "0.1.0" versionName "0.6.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -41,6 +41,9 @@ dependencies {
// GIF // GIF
implementation 'com.github.bumptech.glide:glide:3.8.0' implementation 'com.github.bumptech.glide:glide:3.8.0'
// NanoHTTPD
implementation 'org.nanohttpd:nanohttpd:2.3.1'
// Tests // Tests
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.ext:junit:1.1.5'

View File

@@ -4,6 +4,8 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<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.ACCESS_WIFI_STATE"/>
<application <application
android:allowBackup="true" android:allowBackup="true"

View File

@@ -0,0 +1,316 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Éditeur de Catalogue</title>
<style>
body {
font-family: sans-serif;
padding: 20px;
background: #eee;
margin-bottom: 100px;
}
.card {
background: white;
padding: 20px;
margin-bottom: 30px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
padding: 10px;
border-bottom: 1px solid #ddd;
text-align: left;
}
input[type='text'],
input[type='number'] {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
width: 100%;
box-sizing: border-box;
}
.img-p {
width: 60px;
height: 60px;
object-fit: cover;
background: #f0f0f0;
border-radius: 5px;
}
.btn-add {
background: #0049AF;
color: white;
border: none;
padding: 10px 20px;
cursor: pointer;
margin-top: 10px;
border-radius: 5px;
font-weight: bold;
}
.btn-save {
background: #2E7D32;
color: white;
padding: 15px 30px;
border: none;
border-radius: 50px;
cursor: pointer;
font-size: 18px;
font-weight: bold;
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
width: auto;
white-space: nowrap;
}
.btn-del {
background: #d32f2f;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
padding: 5px 10px;
}
.link-mod {
background: none;
border: none;
color: #0049AF;
text-decoration: underline;
cursor: pointer;
padding: 0;
font-size: 12px;
appearance: none;
-webkit-appearance: none;
outline: none;
}
.actions-bar {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
display: flex;
gap: 15px;
}
.btn-cancel {
background: #F54927;
color: white;
padding: 15px 30px;
border: none;
border-radius: 50px;
cursor: pointer;
font-size: 18px;
font-weight: bold;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
white-space: nowrap;
}
.btn-cat {
background: #0049AF;
color: white;
padding: 15px 25px;
border: none;
border-radius: 50px;
cursor: pointer;
font-size: 18px;
font-weight: bold;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
}
.btn-back {
background: #000000;
color: white;
padding: 15px 25px;
border: none;
border-radius: 50px;
cursor: pointer;
font-size: 18px;
font-weight: bold;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
}
.input-h2 {
font-size: 1.5rem !important;
font-weight: bold !important;
border: none !important;
background: transparent !important;
color: #555 !important;
width: auto;
max-width: 20%;
border-bottom: 1px dashed #ccc !important;
outline: none;
}
.input-h2:focus {
border-bottom: 2px solid #0049AF;
color: #0049AF;
}
</style>
</head>
<body>
<h1>Éditeur de Catalogue</h1>
<form id="editor-form" accept-charset="UTF-8">
{{GENERATED_CONTENT}}
<div class="actions-bar">
<button type="button" class="btn-back" onclick="window.location.href= '/'">← Retour </button>
<button type="button" class="btn-cat" onclick="addCategory()">+ Catégorie</button>
<button type="button" onclick="sendData()" class="btn-save" style="position:static; transform:none;">💾
Enregistrer tout</button>
<button type="button" class="btn-cancel" onclick="handleCancel(event)">✖ Tout annuler</button>
</div>
</form>
<script>
function addRow(cat) {
var t = document.getElementById('table-' + cat);
var r = t.insertRow(-1);
var id = Date.now();
r.innerHTML = '<td>' +
'<img src="" class="img-p" onerror="this.src=\'https://placehold.co/60?text=?\'" style="display:block; margin-bottom:5px;">' +
'<input type="file" accept=".jpg,.png" style="font-size:10px; width:70px" onchange="uImg(this)">' +
'<input type="hidden" name="new|' + cat + '|' + id + '|img" value="">' +
'</td>' +
'<td><input type="text" name="new|' + cat + '|' + id + '|name" placeholder="Nom..." required></td>' +
'<td><input type="number" name="new|' + cat + '|' + id + '|min" value="0" min="0" required></td>' +
'<td><input type="number" name="new|' + cat + '|' + id + '|max" value="0" min="0" required></td>' +
'<td><button type="button" class="btn-del" onclick="var l=this.parentNode.parentNode; l.parentNode.removeChild(l);">×</button></td>';
}
function uImg(element) {
var f = element.files[0];
if (!f) return;
var td = element.parentNode;
var imgTag = td.querySelector('.img-p');
var hiddenInput = td.querySelector('input[type="hidden"]');
var reader = new FileReader();
reader.onload = function(e) {
imgTag.src = e.target.result;
hiddenInput.value = e.target.result;
};
reader.readAsDataURL(f);
}
function handleCancel(e) {
// Avoid form to wait for data to send
if (e) e.preventDefault();
if (confirm("Attention : toutes vos modifications seront perdues. Continuer ?")) {
// Random URL parameters ?t= to force browser to forget previous data
window.location.href = "/edit?t=" + Date.now();
}
}
function addCategory() {
var name = prompt("Nom de la nouvelle catégorie ?");
if (!name || name.trim() === "") return;
// Temp ID
var tempId = "newcat_" + Date.now();
var container = document.createElement('div');
container.className = 'card';
container.innerHTML =
'<h2>' +
'<input type="text" class="input-h2" name="cat|' + tempId + '|name" value="' + name + '">' +
'<div style="display:inline-block; vertical-align:middle; margin: 0 15px;">' +
'<small>Fond:</small><br><input type="color" name="cat|' + tempId + '|bgColor" value="#0049AF" style="width:30px; height:25px; border:none; cursor:pointer;">' +
'</div>' +
'<div style="display:inline-block; vertical-align:middle; margin-right:15px;">' +
'<small>Texte:</small><br><input type="color" name="cat|' + tempId + '|textColor" value="#FFFFFF" style="width:30px; height:25px; border:none; cursor:pointer;">' +
'</div>' +
' <button type="button" class="btn-del" onclick="if(confirm(\'Supprimer cette catégorie ?\')) this.closest(\'.card\').remove()">×</button>' +
'</h2>' +
'<table id="table-' + tempId + '">' +
'<tr><th>Image</th><th>Nom</th><th>Prix Min</th><th>Prix Max</th><th></th></tr>' +
'</table>' +
'<button type="button" class="btn-add" onclick="addRow(\'' + tempId + '\')">+ Ajouter article</button>';
var form = document.querySelector('form');
var actionBar = document.querySelector('.actions-bar');
form.insertBefore(container, actionBar);
container.scrollIntoView({ behavior: 'smooth' });
}
function sendData() {
const form = document.getElementById('editor-form');
const data = {
cat_order_list: "",
items: {}
};
// Browse showed categories
const catOrder = [];
document.querySelectorAll('.card').forEach((card, catIdx) => {
let techId = card.querySelector('input[name^="cat|"]')?.name.split('|')[1] || "c" + catIdx; // c0, c1 but avoid globals
catOrder.push(techId);
const catInput = card.querySelector('input[name$="|name"]');
const bgInput = card.querySelector('input[name$="|bgColor"]');
const textInput = card.querySelector('input[name$="|textColor"]');
if (!catInput) return;
data.items["cat|" + techId + "|name"] = catInput.value;
data.items["cat|" + techId + "|bgColor"] = bgInput ? bgInput.value : "#0049AF";
data.items["cat|" + techId + "|textColor"] = textInput ? textInput.value : "#FFFFFF";
// Browse each line
card.querySelectorAll('table tr').forEach((row, itemIdx) => {
if (itemIdx === 0) return; // Skip title line (th)
const realIdx = itemIdx - 1; // Start at 0
row.querySelectorAll('input').forEach(input => {
const parts = input.name.split('|');
if (parts.length >= 4) {
// parts[0] = "item" ou "new", parts[3] = "name" ou "min" ou "max"
const finalKey = "item|" + techId + "|" + realIdx + "|" + parts[3];
data.items[finalKey] = input.value;
}
});
});
});
data.cat_order_list = catOrder.join(',');
// Create new clean struct : cat|c0|name, item|c0|0|name...
fetch('/save_editor', {
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=UTF-8' },
body: JSON.stringify(data)
})
.then(r => {
if (r.ok) {
alert("✅ Enregistré !");
window.location.reload(); // Optionnel : rafraîchit pour voir le résultat propre
} else {
alert("❌ Erreur serveur");
}
})
.catch(() => alert("❌ Erreur réseau"));
}
</script>
</body>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<style>
body { font-family: sans-serif; line-height: 1.6; padding: 20px; color: #333; max-width: 800px; margin: auto; }
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; margin: 10px 5px; vertical-align: middle;}
.btn-download { background: #2E7D32; }
input[type='file'] { margin: 10px 0; }
.status { font-weight: bold; color: green; }
</style>
</head>
<body>
<h1>Application : stock-pignon</h1>
<h2>Gestion des stocks</h2>
<div class='card'>
<h3>Récupérer les sacoches des adhérent·es</h3>
<p>Télécharger le décompte des pièces sorties de l'atelier.</p>
<a href='/output_json' class='btn'>Voir stock.json</a>
<a href='/output_csv' class='btn'>Voir stock.csv</a>
<a href='/download_output_json' class='btn'>Télécharger stock.json</a>
<a href='/download_output_csv' class='btn'>Télécharger stock.csv</a>
</div>
<h2>Modifier le catalogue</h2>
<div class='card'>
<h3>Gestion du fichier JSON</h3>
<p>Catalogue actuel : <a href='/download_input' class='btn'>Télécharger pieces.json</a></p>
<hr>
<form action='/upload_json' method='post' enctype='multipart/form-data'>
<p><strong>Envoyer un nouveau catalogue :</strong></p>
<input type='file' name='json_file' accept='.json'><br>
<input type='submit' value='Remplacer pieces.json' class='btn'>
</form>
</div>
<div class='card'>
<h3>Gestion des images</h3>
<form action='/upload_images' method='post' enctype='multipart/form-data'>
<p><strong>Ajouter/Modifier des images (PNG/JPG) :</strong></p>
<input type='file' name='images' accept='image/*' multiple><br>
<input type='submit' value='Envoyer les images' class='btn'>
</form>
</div><div class='card'>
<h3>Modifier dans le navigateur</h3>
<p><a href='/edit' class='btn'>Editeur en ligne</a></p>
</div>
</body>
</html>

View File

@@ -8,10 +8,11 @@
"bgColor": "#FF0000", "bgColor": "#FF0000",
"textColor": "#000000", "textColor": "#000000",
"items": [ "items": [
{"name": "Vélo", "minPrice": 20, "maxPrice": 40, "image": "velo"},
{"name": "Cadre", "minPrice": 20, "maxPrice": 40, "image": "cadre"}, {"name": "Cadre", "minPrice": 20, "maxPrice": 40, "image": "cadre"},
{"name": "Selle", "minPrice": 1, "maxPrice": 7, "image": "selle"}, {"name": "Selle", "minPrice": 1, "maxPrice": 7, "image": "selle"},
{"name": "Tige de selle", "minPrice": 1, "maxPrice": 10, "image": "tigeselle"}, {"name": "Tige de selle", "minPrice": 1, "maxPrice": 10, "image": "tige_selle"},
{"name": "Collier de selle", "minPrice": 1, "maxPrice": 4, "image": "collierselle"} {"name": "Collier de selle", "minPrice": 1, "maxPrice": 4, "image": "collier_selle"}
] ]
}, },
{ {
@@ -20,10 +21,14 @@
"textColor": "#000000", "textColor": "#000000",
"items": [ "items": [
{"name": "Fourche", "minPrice": 10, "maxPrice": 20, "image": "fourche"}, {"name": "Fourche", "minPrice": 10, "maxPrice": 20, "image": "fourche"},
{"name": "Jeu de direction", "minPrice": 1, "maxPrice": 8, "image": "jeudirection"}, {"name": "Fourche suspendue", "minPrice": 10, "maxPrice": 20, "image": "fourche_suspendue"},
{"name": "Potence headset", "minPrice": 2, "maxPrice": 8, "image": "potenceheadset"}, {"name": "Roulement direction", "minPrice": 1, "maxPrice": 8, "image": "roulement_direction"},
{"name": "Potence plongeur", "minPrice": 2, "maxPrice": 8, "image": "potenceplongeur"}, {"name": "Jeu de direction", "minPrice": 1, "maxPrice": 8, "image": "jeu_direction"},
{"name": "Tige plongeur", "minPrice": 1, "maxPrice": 4, "image": "tigeplongeur"} {"name": "Potence headset", "minPrice": 2, "maxPrice": 8, "image": "potence_headset"},
{"name": "Potence headset réglable", "minPrice": 2, "maxPrice": 8, "image": "potence_headset_reglable"},
{"name": "Potence plongeur", "minPrice": 2, "maxPrice": 8, "image": "potence_plongeur"},
{"name": "Potence plongeur réglable", "minPrice": 2, "maxPrice": 8, "image": "potence_plongeur_reglable"},
{"name": "Tige plongeur", "minPrice": 1, "maxPrice": 4, "image": "tige_plongeur"}
] ]
}, },
{ {
@@ -31,13 +36,14 @@
"bgColor": "#F16000", "bgColor": "#F16000",
"textColor": "#000000", "textColor": "#000000",
"items": [ "items": [
{"name": "Poignée combo", "minPrice": 2, "maxPrice": 8, "image": "poigneecombo"}, {"name": "Poignée combo", "minPrice": 2, "maxPrice": 8, "image": "poignee_combo"},
{"name": "Poignée frein", "minPrice": 2, "maxPrice": 8, "image": "poigneefrein"}, {"name": "Poignée frein", "minPrice": 2, "maxPrice": 8, "image": "poignee_frein"},
{"name": "Poignée gachette", "minPrice": 2, "maxPrice": 8, "image": "poigneegachette"}, {"name": "Poignée gachette", "minPrice": 2, "maxPrice": 8, "image": "poignee_gachette"},
{"name": "Poignée tournante", "minPrice": 2, "maxPrice": 6, "image": "poigneetournante"}, {"name": "Poignée tournante", "minPrice": 2, "maxPrice": 6, "image": "poignee_tournante"},
{"name": "Manette cadre", "minPrice": 2, "maxPrice": 6, "image": "manette_cadre"},
{"name": "Gaine", "minPrice": 0, "maxPrice": 2, "image": "gaine"}, {"name": "Gaine", "minPrice": 0, "maxPrice": 2, "image": "gaine"},
{"name": "Câble", "minPrice": 0, "maxPrice": 1, "image": "cable"}, {"name": "Câble", "minPrice": 0, "maxPrice": 1, "image": "cable"},
{"name": "Poignée", "minPrice": 1, "maxPrice": 4, "image": "poignees"}, {"name": "Poignée", "minPrice": 1, "maxPrice": 4, "image": "poignee"},
{"name": "Cintre", "minPrice": 5, "maxPrice": 10,"image": "cintre"} {"name": "Cintre", "minPrice": 5, "maxPrice": 10,"image": "cintre"}
] ]
}, },
@@ -46,11 +52,15 @@
"bgColor": "#FF00FF", "bgColor": "#FF00FF",
"textColor": "#000000", "textColor": "#000000",
"items": [ "items": [
{"name": "Étrier patin", "minPrice": 2, "maxPrice": 8, "image": "etrierpatin"}, {"name": "Étrier V-brake", "minPrice": 2, "maxPrice": 8, "image": "etrier_vbrake"},
{"name": "Étrier disque", "minPrice": 2, "maxPrice": 8, "image": "etrierdisque"}, {"name": "Étrier Cantilever", "minPrice": 2, "maxPrice": 8, "image": "etrier_cantilever"},
{"name": "Étrier Caliper ou Mafac", "minPrice": 2, "maxPrice": 8, "image": "etrier_mafac_caliper"},
{"name": "Étrier disque câble", "minPrice": 2, "maxPrice": 8, "image": "etrier_cable"},
{"name": "Étrier disque hydraulique", "minPrice": 2, "maxPrice": 8, "image": "etrier_hydraulique"},
{"name": "Patins", "minPrice": 1, "maxPrice": 2, "image": "patins"}, {"name": "Patins", "minPrice": 1, "maxPrice": 2, "image": "patins"},
{"name": "Disque", "minPrice": 1, "maxPrice": 5, "image": "disque"}, {"name": "Disque", "minPrice": 1, "maxPrice": 5, "image": "disque"},
{"name": "Visserie frein", "minPrice": 0, "maxPrice": 1, "image": "visseriefrein"} {"name": "Pont central cantilever", "minPrice": 0, "maxPrice": 1, "image": "frein_pontcentral"},
{"name": "Visserie frein", "minPrice": 0, "maxPrice": 1, "image": "visserie_frein"}
] ]
}, },
{ {
@@ -58,15 +68,23 @@
"bgColor": "#0000FF", "bgColor": "#0000FF",
"textColor": "#FFFFFF", "textColor": "#FFFFFF",
"items": [ "items": [
{"name": "Roue avant", "minPrice": 20, "maxPrice": 40, "image": "roueavant"}, {"name": "Chambre à air", "minPrice": 20, "maxPrice": 40, "image": "chambre_air"},
{"name": "Roue arrière", "minPrice": 20, "maxPrice": 40, "image": "rouearriere"}, {"name": "Roue avant", "minPrice": 20, "maxPrice": 40, "image": "roue_avant"},
{"name": "Axe rapide", "minPrice": 1, "maxPrice": 2, "image": "axerapide"}, {"name": "Roue arrière", "minPrice": 20, "maxPrice": 40, "image": "roue_arriere"},
{"name": "Axe roue", "minPrice": 1, "maxPrice": 3, "image": "axeroue"}, {"name": "Axe rapide", "minPrice": 1, "maxPrice": 2, "image": "axe_rapide"},
{"name": "Axe roue", "minPrice": 1, "maxPrice": 3, "image": "axe_roue"},
{"name": "Rayon", "minPrice": 0, "maxPrice": 2, "image": "rayon"}, {"name": "Rayon", "minPrice": 0, "maxPrice": 2, "image": "rayon"},
{"name": "Moyeu avant", "minPrice": 2, "maxPrice": 8, "image": "moyeuavant"}, {"name": "Jante", "minPrice": 0, "maxPrice": 2, "image": "jante"},
{"name": "Moyeu arrière", "minPrice": 2, "maxPrice": 10, "image": "moyeuarriere"}, {"name": "Fond de jante", "minPrice": 0, "maxPrice": 2, "image": "fond_jante"},
{"name": "Pneu", "minPrice": 10, "maxPrice": 20, "image": "pneu"}, {"name": "Moyeu avant", "minPrice": 2, "maxPrice": 8, "image": "moyeu_avant"},
{"name": "Visserie roue", "minPrice": 0, "maxPrice": 1, "image": "visserieroue"} {"name": "Moyeu arrière cassette", "minPrice": 2, "maxPrice": 10, "image": "moyeu_arriere_cassette"},
{"name": "Moyeu arrière roue libre", "minPrice": 2, "maxPrice": 10, "image": "moyeu_arriere_rouelibre"},
{"name": "Moyeu arrière rétropédalage", "minPrice": 2, "maxPrice": 10, "image": "moyeu_retropedalage"},
{"name": "Pneu petite taille", "minPrice": 10, "maxPrice": 20, "image": "pneu_petit"},
{"name": "Pneu 26\"", "minPrice": 10, "maxPrice": 20, "image": "pneu_26"},
{"name": "Pneu 28\" route", "minPrice": 10, "maxPrice": 20, "image": "pneu_route"},
{"name": "Pneu 28\" large", "minPrice": 10, "maxPrice": 20, "image": "pneu_large"},
{"name": "Visserie roue", "minPrice": 0, "maxPrice": 1, "image": "visserie_roue"}
] ]
}, },
{ {
@@ -74,12 +92,13 @@
"bgColor": "#FFCC00", "bgColor": "#FFCC00",
"textColor": "#000000", "textColor": "#000000",
"items": [ "items": [
{"name": "Dérailleur avant", "minPrice": 2, "maxPrice": 10, "image": "fd"}, {"name": "Dérailleur arrière", "minPrice": 2, "maxPrice": 10, "image": "derailleur_arriere"},
{"name": "Dérailleur arrière", "minPrice": 4, "maxPrice": 12, "image": "rd"}, {"name": "Dérailleur avant tirage bas", "minPrice": 4, "maxPrice": 12, "image": "derailleur_avant_tiragebas"},
{"name": "Dérailleur avant tirage haut", "minPrice": 4, "maxPrice": 12, "image": "derailleur_avant_tiragebas"},
{"name": "Cassette", "minPrice": 2, "maxPrice": 6, "image": "cassette"}, {"name": "Cassette", "minPrice": 2, "maxPrice": 6, "image": "cassette"},
{"name": "Roue libre", "minPrice": 2, "maxPrice": 6, "image": "rouelibre"}, {"name": "Roue libre", "minPrice": 2, "maxPrice": 6, "image": "roue_libre"},
{"name": "Chaîne", "minPrice": 2, "maxPrice": 6, "image": "chaine"}, {"name": "Chaîne", "minPrice": 2, "maxPrice": 6, "image": "chaine"},
{"name": "Visserie transmission", "minPrice": 0, "maxPrice": 1, "image": "visserietransmission"} {"name": "Visserie transmission", "minPrice": 0, "maxPrice": 1, "image": "visserie_transmission"}
] ]
}, },
{ {
@@ -87,13 +106,17 @@
"bgColor": "#00FFFF", "bgColor": "#00FFFF",
"textColor": "#000000", "textColor": "#000000",
"items": [ "items": [
{"name": "Boîtier pédalier", "minPrice": 4, "maxPrice": 12, "image": "boitierpedalier"}, {"name": "Axe pédalier carré ou clavette", "minPrice": 4, "maxPrice": 12, "image": "axe_pedalier"},
{"name": "Manivelle gauche", "minPrice": 4, "maxPrice": 12, "image": "manivellegauche"}, {"name": "Roulement pédalier", "minPrice": 4, "maxPrice": 12, "image": "roulement_pedalier"},
{"name": "Manivelle droite", "minPrice": 4, "maxPrice": 12, "image": "manivelledroite"}, {"name": "Boîtier pédalier cartouche", "minPrice": 4, "maxPrice": 12, "image": "boitier_cartouche"},
{"name": "Pédalier complet", "minPrice": 6, "maxPrice": 15, "image": "pedaliercomplet"}, {"name": "Boîtier pédalier Hollowtech 2", "minPrice": 4, "maxPrice": 12, "image": "boitier_hollowtech"},
{"name": "Manivelle gauche", "minPrice": 4, "maxPrice": 12, "image": "manivelle_gauche"},
{"name": "Manivelle droite", "minPrice": 4, "maxPrice": 12, "image": "manivelle_droite"},
{"name": "Pédalier complet", "minPrice": 6, "maxPrice": 15, "image": "pedalier_complet"},
{"name": "Pédales", "minPrice": 2, "maxPrice": 8, "image": "pedales"}, {"name": "Pédales", "minPrice": 2, "maxPrice": 8, "image": "pedales"},
{"name": "Plateau", "minPrice": 2, "maxPrice": 8, "image": "plateau"}, {"name": "Plateau 4 branches", "minPrice": 2, "maxPrice": 8, "image": "plateau_4"},
{"name": "Visserie pédalier", "minPrice": 0, "maxPrice": 1, "image": "visseriepedalier"} {"name": "Plateau 5 branches", "minPrice": 2, "maxPrice": 8, "image": "plateau_5"},
{"name": "Visserie pédalier", "minPrice": 0, "maxPrice": 1, "image": "visserie_pedalier"}
] ]
}, },
{ {
@@ -102,10 +125,13 @@
"textColor": "#000000", "textColor": "#000000",
"items": [ "items": [
{"name": "Béquille", "minPrice": 2, "maxPrice": 8, "image": "bequille"}, {"name": "Béquille", "minPrice": 2, "maxPrice": 8, "image": "bequille"},
{"name": "Garde boue", "minPrice": 2, "maxPrice": 8, "image": "gardeboue"}, {"name": "Garde boue", "minPrice": 2, "maxPrice": 8, "image": "garde_boue"},
{"name": "Lampe", "minPrice": 1, "maxPrice": 5, "image": "lampe"}, {"name": "Lampe", "minPrice": 1, "maxPrice": 5, "image": "lampe"},
{"name": "Porte bagage", "minPrice": 2, "maxPrice": 10, "image": "portebagage"}, {"name": "Porte bagage", "minPrice": 2, "maxPrice": 10, "image": "porte_bagage"},
{"name": "Porte bidon", "minPrice": 0, "maxPrice": 1, "image": "portebidon"} {"name": "Porte bidon", "minPrice": 0, "maxPrice": 1, "image": "porte_bidon"},
{"name": "Pompe", "minPrice": 0, "maxPrice": 1, "image": "pompe"},
{"name": "Panier", "minPrice": 0, "maxPrice": 1, "image": "panier"},
{"name": "Rétroviseur", "minPrice": 0, "maxPrice": 1, "image": "retroviseur"}
] ]
} }
] ]

View File

@@ -3,7 +3,6 @@ package com.stock.pignon;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.os.Handler; import android.os.Handler;
import android.os.Environment; import android.os.Environment;
import android.text.Html; import android.text.Html;
@@ -12,7 +11,6 @@ import android.view.View;
import android.widget.GridLayout; import android.widget.GridLayout;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.ImageView;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
@@ -23,11 +21,8 @@ import java.io.FileOutputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.HashMap; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import com.bumptech.glide.Glide;
/** /**
* Action on shopping cart: validation, clearing, and persistence * Action on shopping cart: validation, clearing, and persistence
@@ -36,6 +31,21 @@ public class CartActionHelper {
private static final String TAG = "CartActionHelper"; private static final String TAG = "CartActionHelper";
/**
* Data structure for JSON and CSV
*/
private static class StockEntry {
String date;
String name;
int qty;
StockEntry(String date, String name, int qty) {
this.date = date;
this.name = name;
this.qty = qty;
}
}
/** /**
* Resets the cart data and refreshes UI * Resets the cart data and refreshes UI
*/ */
@@ -93,6 +103,74 @@ public class CartActionHelper {
.show(); .show();
} }
/**
* Merges current cart items with the existing stock file (json and csv) on the SD Card.
*/
private static void saveCartToExternalFile(List<CartItem> cartItems) {
File dir = new File(Environment.getExternalStorageDirectory(), Config.EXTERNAL_DIR_NAME);
File stockFile = new File(dir, Config.OUTPUT_JSON_NAME);
File csvFile = new File(dir, Config.OUTPUT_CSV_NAME);
String today = DateHelper.getTodayIso();
Gson gson = new Gson();
// Load previous list
List<StockEntry> history = loadHistory(stockFile, gson);
// Merge current cart items in previous list
for (CartItem cartItem : cartItems) {
boolean merged = false;
for (StockEntry entry : history) {
// Same date same name ? Add it
if (entry.date.equals(today) && entry.name.equals(cartItem.getName())) {
entry.qty += cartItem.getQuantity();
merged = true;
break;
}
}
// Not found on the same date ? Create it
if (!merged) {
history.add(new StockEntry(today, cartItem.getName(), cartItem.getQuantity()));
}
}
// Save to JSON
try (FileOutputStream fos = new FileOutputStream(stockFile);
OutputStreamWriter writer = new OutputStreamWriter(fos, "UTF-8")) {
gson.newBuilder().setPrettyPrinting().create().toJson(history, writer);
} catch (Exception e) {
Log.e(TAG, "Error writing JSON", e);
}
// Save to CSV, french format with ";"
try (FileOutputStream fos = new FileOutputStream(csvFile);
OutputStreamWriter writer = new OutputStreamWriter(fos, "UTF-8")) {
writer.write('\ufeff');
writer.write("Date;Article;Quantité\n");
for (StockEntry entry : history) {
writer.write(entry.date + ";" + entry.name.replace(";", ",") + ";" + entry.qty + "\n");
}
} catch (Exception e) {
Log.e(TAG, "Error writing CSV", e);
}
}
/**
* Load JSON history
*/
private static List<StockEntry> loadHistory(File file, Gson gson) {
if (!file.exists()) return new ArrayList<>();
try (InputStreamReader reader = new InputStreamReader(new FileInputStream(file), "UTF-8")) {
Type type = new TypeToken<List<StockEntry>>(){}.getType();
List<StockEntry> result = gson.fromJson(reader, type);
return (result != null) ? result : new ArrayList<>();
} catch (Exception e) {
Log.e(TAG, "Error reading json history", e);
return new ArrayList<>();
}
}
/** /**
* Displays a thank you popup and returns to the main menu after 2 seconds. * Displays a thank you popup and returns to the main menu after 2 seconds.
*/ */
@@ -104,78 +182,21 @@ public class CartActionHelper {
.create(); .create();
merciDialog.show(); merciDialog.show();
new Handler().postDelayed(() -> { new Handler().postDelayed(() -> {
// Close dialog // Close dialog
merciDialog.dismiss(); merciDialog.dismiss();
if (activity instanceof MainActivity) {
MainActivity main = (MainActivity) activity;
// Go to home if not already // Go to home if not already
if (!(activity instanceof MainActivity)) { main.showHome();
Intent intent = new Intent(activity, MainActivity.class);
// Clear the backstack so the user can't "go back" to a validated cart // Clear the backstack so the user can't "go back" to a validated cart
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); GridLayout grid = main.findViewById(R.id.gridPieces);
activity.startActivity(intent); refreshGridQuantities(grid);
} }
}, 2000); }, 2000);
} }
/**
* Merges current cart items with the existing stock file on the SD Card.
*/
private static void saveCartToExternalFile(List<CartItem> cartItems) {
File stockFile = new File(Environment.getExternalStorageDirectory(), Config.STOCK_FILE_NAME);
Gson gson = new Gson();
Map<String, Integer> stockMap;
// Load data into a Map (Key: Item Name, Value: Total Quantity)
stockMap = loadExistingStock(stockFile, gson);
// Merge with new items from current cart
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
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);
}
}
/**
* Reads the current stock file. If the file is missing or corrupted, returns an empty map.
*/
private static Map<String, Integer> loadExistingStock(File stockFile, Gson gson) {
if (!stockFile.exists()) return new HashMap<>();
try (FileInputStream fis = new FileInputStream(stockFile);
@SuppressWarnings("CharsetObjectCanBeUsed")
InputStreamReader reader = new InputStreamReader(fis, "UTF-8")) {
// Type definition for Map required by GSON : <String, Integer>
Type type = new TypeToken<Map<String, Integer>>(){}.getType();
// Read and fill map
Map<String, Integer> result = gson.fromJson(reader, type);
return (result != null) ? result : new HashMap<>();
} catch (Exception e) {
Log.e(TAG, "Error reading existing stock, starting fresh", e);
return new HashMap<>();
}
}
/** /**
* Updates the UI grid to reflect quantities. * Updates the UI grid to reflect quantities.
*/ */

View File

@@ -20,8 +20,10 @@ public class CartItem {
public int getMinPrice() { return minPrice; } public int getMinPrice() { return minPrice; }
public int getMaxPrice() { return maxPrice; } public int getMaxPrice() { return maxPrice; }
public int getQuantity() { return quantity; } public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
public int getTotalMin() { return minPrice * quantity; } public int getTotalMin() { return minPrice * quantity; }
public int getTotalMax() { return maxPrice * quantity; } public int getTotalMax() { return maxPrice * quantity; }
public String getImageFile() { return imageFile; } public String getImageFile() { return imageFile; }
// Setters
public void setQuantity(int quantity) { this.quantity = quantity; }
} }

View File

@@ -8,10 +8,14 @@ public class CartManager {
// Unified and global list for whole application // Unified and global list for whole application
private static final List<CartItem> items = new ArrayList<>(); private static final List<CartItem> items = new ArrayList<>();
// Private constructor: utility class, it should not be instantiated /**
* Private constructor: utility class, it should not be instantiated
*/
private CartManager() {} private CartManager() {}
// Returns the direct reference to the list to save memory on older devices /**
* Returns the direct reference to the list to save memory on older devices
*/
public static List<CartItem> getItems() { public static List<CartItem> getItems() {
return items; return items;
} }
@@ -25,7 +29,9 @@ public class CartManager {
return null; return null;
} }
// Logic for adding, updating or removing items based on quantity, prevents duplicate entries /**
* Logic for adding, updating or removing items based on quantity, prevents duplicate entries
*/
public static void addOrUpdateItem(String name, int minPrice, int maxPrice, int quantity, String imageFile) { public static void addOrUpdateItem(String name, int minPrice, int maxPrice, int quantity, String imageFile) {
CartItem current = getItemByName(name); CartItem current = getItemByName(name);
@@ -40,20 +46,27 @@ public class CartManager {
} }
} }
// Range-based pricing /**
* Range-based pricing
*/
public static int getGlobalTotalMin() { public static int getGlobalTotalMin() {
int total = 0; int total = 0;
for (CartItem item : items) total += item.getTotalMin(); for (CartItem item : items) total += item.getTotalMin();
return total; return total;
} }
/**
* Range-based pricing
*/
public static int getGlobalTotalMax() { public static int getGlobalTotalMax() {
int total = 0; int total = 0;
for (CartItem item : items) total += item.getTotalMax(); for (CartItem item : items) total += item.getTotalMax();
return total; return total;
} }
/**
* Remove item from cart
*/
public static void clear() { public static void clear() {
items.clear(); items.clear();
} }

View File

@@ -22,17 +22,16 @@ import java.util.List;
public class CartViewHelper { public class CartViewHelper {
/** /**
* Refreshes the entire cart side-panel or list * Refreshes cart UI by synchronizing the visual list with the current CartManager state
* Clears everything and rebuild on current CartManager data
*/ */
public static void updateCartView(LinearLayout cartList, Context context) { public static void updateCartView(LinearLayout cartList, Context context) {
if (cartList == null) return; if (cartList == null) return;
// Clear and get current items // Clean item lists
cartList.removeAllViews(); cartList.removeAllViews();
List<CartItem> cartItems = CartManager.getItems(); List<CartItem> cartItems = CartManager.getItems();
// If empty cart, show it to user // Manage empty cart
if (cartItems.isEmpty()) { if (cartItems.isEmpty()) {
TextView empty = new TextView(context); TextView empty = new TextView(context);
empty.setText(context.getString(R.string.cart_empty)); empty.setText(context.getString(R.string.cart_empty));
@@ -40,36 +39,20 @@ public class CartViewHelper {
empty.setGravity(Gravity.CENTER); empty.setGravity(Gravity.CENTER);
empty.setPadding(0, 50, 0, 0); empty.setPadding(0, 50, 0, 0);
cartList.addView(empty); cartList.addView(empty);
updateTotalDisplay(context, 0, 0); updateTotalDisplay(context, 0, 0);
return; } else {
} // Create list if not empty
// Build list and calculate total at the same time
int totalMin = 0; int totalMin = 0;
int totalMax = 0; int totalMax = 0;
for (CartItem item : cartItems) { for (CartItem item : cartItems) {
totalMin += item.getTotalMin(); totalMin += item.getTotalMin();
totalMax += item.getTotalMax(); totalMax += item.getTotalMax();
// Create and add the row view // Create a line for each item
cartList.addView(createCartItemView(item, cartList, context)); cartList.addView(createCartItemView(item, cartList, context));
} }
// Display global price range at the end
updateTotalDisplay(context, totalMin, totalMax); updateTotalDisplay(context, totalMin, totalMax);
} }
/**
* Updates the global price range at the bottom of the screen
*/
private static void updateTotalDisplay(Context context, int min, int max) {
TextView totalView = ((Activity) context).findViewById(R.id.totalView);
if (totalView != null) {
// Using string resources
totalView.setText(context.getString(R.string.price_range, min, max, ""));
}
} }
/** /**
@@ -81,7 +64,6 @@ public class CartViewHelper {
row.setOrientation(LinearLayout.HORIZONTAL); row.setOrientation(LinearLayout.HORIZONTAL);
row.setGravity(Gravity.CENTER_VERTICAL); row.setGravity(Gravity.CENTER_VERTICAL);
// Layout parameters
LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT); LinearLayout.LayoutParams.WRAP_CONTENT);
@@ -96,7 +78,7 @@ public class CartViewHelper {
image.setLayoutParams(imgParams); image.setLayoutParams(imgParams);
row.addView(image); row.addView(image);
// Sublayout for name + [quantity + price] // Sublayout infos
LinearLayout infoLayout = new LinearLayout(context); LinearLayout infoLayout = new LinearLayout(context);
infoLayout.setOrientation(LinearLayout.VERTICAL); infoLayout.setOrientation(LinearLayout.VERTICAL);
infoLayout.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); infoLayout.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f));
@@ -108,7 +90,7 @@ public class CartViewHelper {
nameView.setTypeface(null, Typeface.BOLD); nameView.setTypeface(null, Typeface.BOLD);
infoLayout.addView(nameView); infoLayout.addView(nameView);
// Quantity and price range // Quantity / price
TextView detailsView = new TextView(context); TextView detailsView = new TextView(context);
String details = context.getString(R.string.cart_item, item.getQuantity(), item.getTotalMin(), item.getTotalMax()); String details = context.getString(R.string.cart_item, item.getQuantity(), item.getTotalMin(), item.getTotalMax());
detailsView.setText(details); detailsView.setText(details);
@@ -124,42 +106,35 @@ public class CartViewHelper {
} }
/** /**
* Creates the delete button * Creates the delete button for each item
*/ */
private static Button createRemoveButton(CartItem item, LinearLayout cartList, Context context) { private static Button createRemoveButton(CartItem item, LinearLayout cartList, Context context) {
Button btn = new Button(context); Button btn = new Button(context);
btn.setText(""); btn.setText("");
btn.setTextSize(18);
btn.setTextColor(Color.WHITE); btn.setTextColor(Color.WHITE);
btn.setGravity(Gravity.CENTER);
// Conversion DP to PX for consistent size on all screens
float scale = context.getResources().getDisplayMetrics().density; float scale = context.getResources().getDisplayMetrics().density;
int sizePx = (int) (48 * scale + 0.5f); int sizePx = (int) (48 * scale + 0.5f);
btn.setLayoutParams(new LinearLayout.LayoutParams(sizePx, sizePx));
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(sizePx, sizePx);
btn.setLayoutParams(params);
// Background: grey rounded rectangle (API 17 fallback)
GradientDrawable shape = new GradientDrawable(); GradientDrawable shape = new GradientDrawable();
shape.setColor(Color.parseColor("#E53935")); // Reddish to indicate delete shape.setColor(Color.parseColor("#E53935"));
shape.setCornerRadius(4 * scale); shape.setCornerRadius(4 * scale);
btn.setBackground(shape); btn.setBackground(shape);
btn.setOnClickListener(v -> { btn.setOnClickListener(v -> {
// setting quantity to 0 removes the item CartManager.addOrUpdateItem(item.getName(), item.getMinPrice(), item.getMaxPrice(), 0, item.getImageFile());
CartManager.addOrUpdateItem(
item.getName(),
item.getMinPrice(),
item.getMaxPrice(),
0, // Setting quantity to 0 triggers removal
item.getImageFile()
);
// visual refresh of the cart list
updateCartView(cartList, context); updateCartView(cartList, context);
}); });
return btn; return btn;
} }
/**
* Manage price range for whole cart
*/
private static void updateTotalDisplay(Context context, int min, int max) {
TextView totalView = ((Activity) context).findViewById(R.id.totalView);
if (totalView != null) {
totalView.setText(context.getString(R.string.price_range, min, max, ""));
}
}
} }

View File

@@ -14,12 +14,25 @@ public class Category {
@SuppressWarnings("unused") @SuppressWarnings("unused")
private List<Item> items; private List<Item> items;
// Empty constructor for GSON
public Category() {}
// Full constructor for online editor
public Category(String name, List<Item> items) {
this.name = name;
this.items = items;
// Default colors
this.bgColor = "#0049AF";
this.textColor = "#FFFFFF";
}
// Getters
public String getName() { return name; } public String getName() { return name; }
public String getBgColor() { return bgColor; } public String getBgColor() { return bgColor; }
public String getTextColor() { return textColor; } public String getTextColor() { return textColor; }
public List<Item> getItems() { return items != null ? items : new ArrayList<>(); }
// Avoid crash if json isn't readable // Setters
public List<Item> getItems() { public void setBgColor(String bgColor) { this.bgColor = bgColor; }
return items != null ? items : new ArrayList<>(); public void setTextColor(String textColor) { this.textColor = textColor; }
}
} }

View File

@@ -9,8 +9,11 @@ public class Config {
public static final String IMAGES_SUBDIR_NAME = "images"; public static final String IMAGES_SUBDIR_NAME = "images";
// Input json // Input json
public static final String PIECES_FILE_NAME = "pieces.json"; public static final String INPUT_JSON_NAME = "pieces.json";
// Output json // Output json
public static final String STOCK_FILE_NAME = "stock.json"; public static final String OUTPUT_JSON_NAME = "stock.json";
// Output json
public static final String OUTPUT_CSV_NAME = "stock.csv";
} }

View File

@@ -0,0 +1,450 @@
// ControlServer.java
package com.stock.pignon;
import android.content.Context;
import android.os.Environment;
import android.util.Log;
import org.json.JSONObject;
import fi.iki.elonen.NanoHTTPD;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import java.util.Scanner;
/**
* Create a web server to remote management of app assets.
* Inherit what is needed to build a web server from NanoHTTPD
*/
public class ControlServer extends NanoHTTPD {
private final Context context;
// Assets folders
private final File storageRoot = Environment.getExternalStorageDirectory();
private final File baseDir = new File(storageRoot, Config.EXTERNAL_DIR_NAME);
private final File imagesDir = new File(baseDir, Config.IMAGES_SUBDIR_NAME);
public ControlServer(Context context, int port) {
super(port);
this.context = context;
}
/**
* Main method which manage requests
*/
@Override
public Response serve(IHTTPSession session) {
// Get requested address
String uri = session.getUri();
if (uri.startsWith("/img_view/")) {
return serveImage(uri.replace("/img_view/", ""));
}
if (session.getMethod() == Method.GET) {
switch (uri) {
// Soon deprecated with online editor
case "/download_input":
return downloadFile(Config.INPUT_JSON_NAME);
// Soon deprecated with online editor
case "/output_json":
return viewFile(Config.OUTPUT_JSON_NAME);
case "/download_output_json":
return downloadFile(Config.OUTPUT_JSON_NAME);
case "/output_csv":
return viewFile(Config.OUTPUT_CSV_NAME);
case "/download_output_csv":
return downloadFile(Config.OUTPUT_CSV_NAME);
case "/edit":
return newFixedLengthResponse(fillEditor());
case "/":
return newFixedLengthResponse(getHtml("index.html"));
}
}
if (session.getMethod() == Method.POST) {
switch (uri) {
// Soon deprecated with online editor
case "/upload_json":
return handleJsonUpload(session);
// Soon deprecated with online editor
case "/upload_images":
return handleImagesUpload(session);
case "/save_editor":
return handleSaveEditor(session);
}
}
return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "❌ Page non trouvée");
}
/**
* Read from asset and return html file
*/
private String getHtml(String file) {
try {
InputStream is = context.getAssets().open(file);
// Scanner html file from the beginning with "\\A"
Scanner s = new Scanner(is, "UTF-8").useDelimiter("\\A");
String html = s.hasNext() ? s.next() : "";
is.close();
return html;
} catch (IOException e) {
return "<html><body>❌ Erreur de chargement de la page</body></html>";
}
}
/**
* Read from asset and return html file
*/
private String fillEditor() {
// Get data from assets
DataLoader.loadData();
// Create an html string to fill the template
StringBuilder html = new StringBuilder();
// Save categories order
List<String> orderList = new ArrayList<>();
// Globals
String globalId = "global";
Category globalCat = new Category("global", DataLoader.getGlobalItems());
html.append(renderCategorySection(globalId, globalCat));
orderList.add(globalId);
// Browser categories
int index = 0;
for (Category cat : DataLoader.getCategories()) {
// cat_0, cat_1...
String techId = "cat_" + index;
html.append(renderCategorySection(techId, cat));
orderList.add(techId);
index++;
}
String orderField = "<input type='hidden' name='cat_order_list' value='" +
android.text.TextUtils.join(",", orderList) + "'>";
return getHtml("editor.html").replace("{{GENERATED_CONTENT}}", html + orderField);
}
/**
* Generate HTML section for each category
*/
private String renderCategorySection(String techId, Category cat) {
String displayName = cat.getName();
List<Item> items = cat.getItems();
StringBuilder sb = new StringBuilder();
sb.append("<div class='card'>");
sb.append("<h2>");
if ("global".equals(displayName)) {
sb.append("Articles globaux");
sb.append("<input type='hidden' name='cat|global|name' value='global'>");
techId = "global";
} else {
// Nom
sb.append("<input type='text' name='cat|").append(techId).append("|name' value=\"").append(displayName).append("\" class='input-h2'>");
// Couleur de Fond (on utilise cat.getBgColor())
sb.append("<div style='display:inline-block; vertical-align:middle; margin-right:15px;'>");
sb.append("<div style='font-weight: bold; font-size: 0.7em; padding-bottom: 5px;'>Fond</div>");
sb.append("<input type='color' name='cat|").append(techId).append("|bgColor' value='").append(cat.getBgColor()).append("' style='width:30px; height:25px; border:none; cursor:pointer;'>");
sb.append("</div>");
// Couleur de Texte (on utilise cat.getTextColor())
sb.append("<div style='display:inline-block; vertical-align:middle; margin-right:15px;'>");
sb.append("<div style='font-weight: bold; font-size: 0.7em; padding-bottom: 5px;'>Texte</div>");
sb.append("<input type='color' name='cat|").append(techId).append("|textColor' value='").append(cat.getTextColor()).append("' style='width:30px; height:25px; border:none; cursor:pointer;'>");
sb.append("</div>");
// Bouton supprimer
sb.append(" <button type='button' class='btn-del' style='vertical-align: middle; margin-left: 10px;' ")
.append("onclick=\"if(confirm('Supprimer cette catégorie?')) this.closest('.card').remove()\">×</button>");
}
sb.append("</h2>");
// Build table
sb.append("<table id='table-").append(techId).append("'>");
sb.append("<tr><th>Image</th><th>Nom</th><th>Prix Min</th><th>Prix Max</th></tr>");
int rowIdx = 0;
for (Item item : items) {
sb.append(renderItemRow(techId, item, rowIdx++));
}
sb.append("</table>");
// Button to add item in this category
sb.append("<button type='button' class='btn-add' onclick=\"addRow('").append(techId).append("')\">+ Ajouter article</button>");
sb.append("</div>");
return sb.toString();
}
/**
* Generate HTML row for each item
*/
private String renderItemRow(String techId, Item item, int idx) {
String prefix = "item|" + techId + "|" + idx;
return "<tr>" +
"<td><img src='/img_view/" + item.getImage() + "' class='img-p' onerror=\"this.src='https://placehold.co/60?text=?'\">" +
"<br><input type='file' accept='.jpg,.png' style='font-size:10px; width:70px' onchange='uImg(this)'>" +
"<input type='hidden' name='" + prefix + "|img' value='" + item.getImage() + "'></td>" +
"<td><input type='text' name='" + prefix + "|name' value=\"" + item.getName() + "\"></td>" +
"<td><input type='number' name='" + prefix + "|min' value='" + item.getMinPrice() + "'></td>" +
"<td><input type='number' name='" + prefix + "|max' value='" + item.getMaxPrice() + "'></td>" +
// Retro JS compatibility
"<td><button type='button' class='btn-del' onclick='var r=this.parentNode.parentNode; r.parentNode.removeChild(r);'>×</button></td>" +
"</tr>";
}
private Response serveImage(String filename) {
File imgJpg = new File(imagesDir, filename + ".jpg");
File imgPng = new File(imagesDir, filename + ".png");
File file = imgJpg.exists() ? imgJpg : (imgPng.exists() ? imgPng : null);
if (file == null || !file.exists()) return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "No image");
try {
Response res = newChunkedResponse(Response.Status.OK, "image/jpeg", new FileInputStream(file));
// Avoid cache if user change image
res.addHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.addHeader("Pragma", "no-cache");
res.addHeader("Expires", "0");
return res;
} catch (IOException e) {
return newFixedLengthResponse("Error");
}
}
/**
* Receive JSON and update cache and storage catalog
*/
private Response handleSaveEditor(IHTTPSession session) {
try {
// NanoHTTPD read request
Map<String, String> files = new HashMap<>();
session.parseBody(files);
String jsonStr = files.get("postData");
if (jsonStr == null || jsonStr.isEmpty()) {
return newFixedLengthResponse(Response.Status.BAD_REQUEST, MIME_PLAINTEXT, "Données vides");
}
JSONObject fullJson = new JSONObject(jsonStr);
// Get items
JSONObject allFields = fullJson.getJSONObject("items");
// Get categories order
String[] orderedIds = fullJson.getString("cat_order_list").split(",");
List<Category> finalData = new ArrayList<>();
// For each category
for (String techId : orderedIds) {
String catNameKey = "cat|" + techId + "|name";
// Does it exist
if (!allFields.has(catNameKey)) continue;
String name = allFields.getString(catNameKey);
List<Item> itemsInCategory = new ArrayList<>();
// Browse and create items
int i = 0;
while (true) {
String itemBase = "item|" + techId + "|" + i + "|";
// On vérifie si l'item suivant existe (via son champ name)
if (!allFields.has(itemBase + "name")) break;
// Manage image
String imgVal = allFields.optString(itemBase + "img", "");
if (imgVal.startsWith("data:image")) {
try {
String newImgName = "img_" + System.currentTimeMillis() + "_" + i;
String base64Data = imgVal.split(",")[1];
byte[] decoded = android.util.Base64.decode(base64Data, android.util.Base64.DEFAULT);
File imageFile = new File(imagesDir, newImgName + ".jpg");
try (FileOutputStream fos = new FileOutputStream(imageFile)) {
fos.write(decoded);
}
imgVal = newImgName; // Replace base64 with name of created jpg file
} catch (Exception e) { Log.e("ControlServer", "Img Error", e); }
}
itemsInCategory.add(new Item(
// Mandatory
allFields.getString(itemBase + "name"),
// Not mandatory
imgVal,
parseSafely(allFields.optString(itemBase + "min", "0")),
parseSafely(allFields.optString(itemBase + "max", "0"))
));
i++;
}
// Create category with items and colors
Category cat = new Category(name, itemsInCategory);
cat.setBgColor(allFields.optString("cat|" + techId + "|bgColor", "#0049AF"));
cat.setTextColor(allFields.optString("cat|" + techId + "|textColor", "#FFFFFF"));
finalData.add(cat);
}
DataLoader.saveData(finalData);
return newFixedLengthResponse(Response.Status.OK, MIME_PLAINTEXT, "OK");
} catch (Exception e) {
Log.e("ControlServer", "Erreur save", e);
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "Erreur");
}
}
private int parseSafely(String val) {
if (val == null) return 0;
try {
return Integer.parseInt(val.trim());
} catch (Exception e) {
return 0;
}
}
// Soon deprecated with online editor
/**
* Manage JSON sent by remote.
*/
private Response handleJsonUpload(IHTTPSession session) {
Map<String, String> tmpFiles = new HashMap<>();
try {
// NanoHTTPD stores loaded file in temp file
session.parseBody(tmpFiles);
// tmpFiles map stores file name and path to tmp folder
String tmpPath = tmpFiles.get("json_file");
if (tmpPath == null) return newFixedLengthResponse("❌ Aucun fichier reçu.");
File src = new File(tmpPath);
File dest = new File(baseDir, Config.INPUT_JSON_NAME);
copyFile(src, dest);
return newFixedLengthResponse("✅ Catalogue mis à jour ! <a href='/'>Retour</a>");
} catch (Exception e) {
return newFixedLengthResponse("❌ Erreur JSON : " + e.getMessage());
}
}
// Soon deprecated with online editor
/**
* Manage multiple images sent by remote.
*/
private Response handleImagesUpload(IHTTPSession session) {
Map<String, String> tmpFiles = new HashMap<>();
try {
session.parseBody(tmpFiles);
if (!imagesDir.exists() && !imagesDir.mkdirs()) {
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT,
"❌ Erreur : Impossible de créer le dossier des images sur la tablette.");
}
int count = 0;
// Browse temp files created by NanoHTTPD
for (Map.Entry<String, String> entry : tmpFiles.entrySet()) {
// Manage multiple files uppload
if (entry.getKey().startsWith("images")) {
String tmpPath = entry.getValue();
// Get file name
List<String> params = session.getParameters().get(entry.getKey());
if (params != null && !params.isEmpty()) {
// Clean file name to remove path, only keep image.jpg or image.png
String originalName = new File(params.get(0)).getName();
File src = new File(tmpPath);
File dest = new File(imagesDir, originalName);
copyFile(src, dest);
count++;
}
}
}
return newFixedLengthResponse("" + count + " images enregistrées ! <a href='/'>Retour</a>");
} catch (Exception e) {
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT,
"❌ Erreur serveur : " + e.getMessage());
}
}
// Soon deprecated with online editor
/**
* Utility method to copy file byte per byte.
* Not always possible to simply move a file on Android.
*/
private void copyFile(File src, File dst) throws IOException {
try (InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
}
}
/**
* 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é.");
}
try {
InputStream is = new FileInputStream(file);
// Force download
Response res = newChunkedResponse(Response.Status.OK, "application/octet-stream", is);
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.");
}
}
}

View File

@@ -4,22 +4,30 @@ package com.stock.pignon;
import android.os.Environment; import android.os.Environment;
import android.util.Log; import android.util.Log;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.util.ArrayList; import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.util.List; import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.LinkedHashMap;
public class DataLoader { public class DataLoader {
private static final String TAG = "DataLoader"; // For readable logs private static final String TAG = "DataLoader"; // For readable logs
private static final String EXTERNAL_DIR = Config.EXTERNAL_DIR_NAME; 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 // Raw data
private static List<Category> cachedCategories = new ArrayList<>(); private static List<Category> cachedCategories = new ArrayList<>();
private static List<Item> cachedGlobals = new ArrayList<>(); private static List<Item> cachedGlobals = new ArrayList<>();
/**
* Get data from input JSON PIECES_FILE
*/
public static void loadData() { public static void loadData() {
File dir = new File(Environment.getExternalStorageDirectory(), EXTERNAL_DIR); File dir = new File(Environment.getExternalStorageDirectory(), EXTERNAL_DIR);
File jsonFile = new File(dir, PIECES_FILE); File jsonFile = new File(dir, PIECES_FILE);
@@ -56,7 +64,10 @@ public class DataLoader {
} }
} }
// Compromise between CPU usage and memory usage : keep cache raw data and combinate global and specific items at call /**
* Create a merge list with global items and items from a specified category for main activity
* Compromise between CPU usage and memory usage : keep cache raw data and combinate global and specific items at call
*/
public static List<Item> getItemsForCategory(String categoryName) { public static List<Item> getItemsForCategory(String categoryName) {
// Add global items // Add global items
@@ -72,13 +83,68 @@ public class DataLoader {
return combinedList; return combinedList;
} }
/**
* To fill online editor, create a map with all items sorted by category
*/
public static Map<String, List<Item>> getAllSections() {
// LinkedHashMap remember item order instead of HashMap
Map<String, List<Item>> sections = new LinkedHashMap<>();
sections.put("global", cachedGlobals);
for (Category cat : cachedCategories) {
sections.put(cat.getName(), cat.getItems());
}
return sections;
}
/**
* Create a web server to remote management of app assets.
*/
public static List<Category> getCategories() { public static List<Category> getCategories() {
return cachedCategories; return cachedCategories;
} }
public static List<Item> getGlobalItems() {
return cachedGlobals;
}
// Internal class for Gson /**
* Internal class for GSON
*/
private static class CategoriesWrapper { private static class CategoriesWrapper {
List<Item> globalItems; List<Item> globalItems;
List<Category> categories; List<Category> categories;
} }
/**
* Write JSON from online editor data
*/
public static void saveData(List<Category> categoriesList) throws Exception {
CategoriesWrapper wrapper = new CategoriesWrapper();
wrapper.categories = new ArrayList<>();
wrapper.globalItems = new ArrayList<>();
// Browse category
for (Category cat : categoriesList) {
if ("global".equals(cat.getName())) {
wrapper.globalItems = cat.getItems();
} else {
wrapper.categories.add(cat);
}
}
// Create JSON
Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
String jsonString = gson.toJson(wrapper);
File dir = new File(Environment.getExternalStorageDirectory(), Config.EXTERNAL_DIR_NAME);
File jsonFile = new File(dir, Config.INPUT_JSON_NAME);
// Write JSON
try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(jsonFile), "UTF-8")) {
writer.write(jsonString);
}
// Update app cache
cachedGlobals = wrapper.globalItems;
cachedCategories = wrapper.categories;
}
} }

View File

@@ -0,0 +1,15 @@
// DateHelper.java
package com.stock.pignon;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
public class DateHelper {
/**
* Return ISO format date (AAAA-MM-JJ)
*/
public static String getTodayIso() {
return new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date());
}
}

View File

@@ -14,9 +14,24 @@ public class Item {
// Empty constructor for GSON // Empty constructor for GSON
public Item() {} public Item() {}
// Full constructor for online editor
public Item(String name, String image, int minPrice, int maxPrice) {
this.name = name;
this.image = image;
this.minPrice = minPrice;
this.maxPrice = maxPrice;
}
// Getters
public String getName() { return name; } public String getName() { return name; }
public int getMinPrice() { return minPrice; } public int getMinPrice() { return minPrice; }
public int getMaxPrice() { return maxPrice; } public int getMaxPrice() { return maxPrice; }
public String getImage() { return image; } public String getImage() { return image; }
// Setters
public void setName(String name) { this.name = name; }
public void setMinPrice(int minPrice) { this.minPrice = minPrice; }
public void setMaxPrice(int maxPrice) { this.maxPrice = maxPrice; }
public void setImage(String image) { this.image = image; }
} }

View File

@@ -1,3 +1,4 @@
// MainActivity.java
package com.stock.pignon; package com.stock.pignon;
import android.graphics.Color; import android.graphics.Color;
@@ -11,6 +12,7 @@ import android.widget.Button;
import android.widget.GridLayout; import android.widget.GridLayout;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.FrameLayout;
import android.widget.TextView; import android.widget.TextView;
import android.os.Environment; import android.os.Environment;
import java.io.File; import java.io.File;
@@ -31,14 +33,17 @@ public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity"; private static final String TAG = "MainActivity";
// UI Components // UI components
private LinearLayout cartList; private LinearLayout cartList;
private LinearLayout homeLayout; private LinearLayout homeLayout;
private LinearLayout categoryItemsLayout; private FrameLayout categoryItemsLayout;
private LinearLayout categoriesLayout; private LinearLayout categoriesLayout;
private GridLayout gridPieces; private GridLayout gridPieces;
private ImageView mainImage; private ImageView mainImage;
// Server component
private ControlServer server;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -57,12 +62,6 @@ public class MainActivity extends AppCompatActivity {
gridPieces = findViewById(R.id.gridPieces); gridPieces = findViewById(R.id.gridPieces);
mainImage = findViewById(R.id.mainImage); mainImage = findViewById(R.id.mainImage);
// Setup Back Button
Button backBtn = findViewById(R.id.backToHomeBtn);
if (backBtn != null) {
backBtn.setOnClickListener(v -> showHome());
}
// Copy assets to sd card if not founded // Copy assets to sd card if not founded
copyAssetsIfEmpty(); copyAssetsIfEmpty();
// Get data from sd card // Get data from sd card
@@ -74,6 +73,71 @@ public class MainActivity extends AppCompatActivity {
// Initial cart visual sync // Initial cart visual sync
CartViewHelper.updateCartView(cartList, this); CartViewHelper.updateCartView(cartList, this);
// Action bar
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayShowHomeEnabled(true);
getSupportActionBar().setTitle(" 🚲 Atelier du Pignon - Gestion du stock à prix libre");
}
// Get app version
String versionName;
try {
versionName = "App v" + getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
} catch (Exception e) {
versionName = ""; // Fallback
}
// Launch server
server = new ControlServer(this,8080);
try {
server.start();
String url = "http://" + getDeviceIP() + ":8080";
// Print URL in action bar for user
if (getSupportActionBar() != null) {
getSupportActionBar().setSubtitle("Serveur en ligne : " + url + " - " + versionName);
}
Log.i(TAG, "Server started on : " + url);
} catch (IOException e) {
if (getSupportActionBar() != null) {
getSupportActionBar().setSubtitle("Erreur serveur" + " - " + versionName);
}
}
// Back button (blue)
Button btnBack = findViewById(R.id.btnBackFromCategory);
if (btnBack != null) {
btnBack.setOnClickListener(v -> showHome());
}
// Empty button (red)
Button btnClear = findViewById(R.id.clearCartBtn);
if (btnClear != null) {
btnClear.setOnClickListener(v -> CartActionHelper.emptyCart(cartList, this));
}
// Validate button (green)
Button btnValidate = findViewById(R.id.validateCartBtn);
if (btnValidate != null) {
btnValidate.setOnClickListener(v -> CartActionHelper.validateCart(cartList, this));
}
}
@Override
protected void onResume() {
super.onResume();
// Ensure cart is up to date if returning to the activity
CartViewHelper.updateCartView(cartList, this);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (server != null) {
server.stop();
Log.i(TAG, "Server stopped.");
}
} }
/** /**
@@ -215,7 +279,7 @@ public class MainActivity extends AppCompatActivity {
CartViewHelper.updateCartView(cartList, this); CartViewHelper.updateCartView(cartList, this);
} }
private void showHome() { public void showHome() {
categoryItemsLayout.setVisibility(View.GONE); categoryItemsLayout.setVisibility(View.GONE);
homeLayout.setVisibility(View.VISIBLE); homeLayout.setVisibility(View.VISIBLE);
} }
@@ -225,46 +289,70 @@ public class MainActivity extends AppCompatActivity {
ImageLoader.loadImage(mainImage, "_velo", 800,800); ImageLoader.loadImage(mainImage, "_velo", 800,800);
} }
// --- Button Actions (linked via android:onClick in XML) ---
public void emptyCart(View view) {
CartActionHelper.emptyCart(cartList, this);
}
public void validateCart(View view) {
CartActionHelper.validateCart(cartList, this);
}
private int dpToPx(int dp) { private int dpToPx(int dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics()); return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
} }
@Override
protected void onResume() {
super.onResume();
// Ensure cart is up to date if returning to the activity
CartViewHelper.updateCartView(cartList, this);
}
private void copyAssetsIfEmpty() { private void copyAssetsIfEmpty() {
File folder = new File(Environment.getExternalStorageDirectory(), Config.EXTERNAL_DIR_NAME); File rootDir = new File(Environment.getExternalStorageDirectory(), Config.EXTERNAL_DIR_NAME);
// First safety : is folder already in sdcard ? // Create root folder if not found
if (!folder.exists()) { if (!rootDir.exists()) {
// Second safety : are we able to create folder ? if (!rootDir.mkdirs()) {
if (folder.mkdirs()) { Log.e("MainActivity", "Can't create root dir." + rootDir.getAbsolutePath());
// Copy JSON file return;
copyFileFromAssets(Config.PIECES_FILE_NAME, new File(folder, Config.PIECES_FILE_NAME));
// Copy images subfolder
File imgFolder = new File(folder, Config.IMAGES_SUBDIR_NAME);
if (imgFolder.mkdirs()) {
copyFolderFromAssets(Config.IMAGES_SUBDIR_NAME, imgFolder);
} }
} }
// Check pieces.json
File inputJson = new File(rootDir, Config.INPUT_JSON_NAME);
if (!inputJson.exists()) {
Log.i("MainActivity", "pieces.json not found, copying it...");
copyFileFromAssets(Config.INPUT_JSON_NAME, inputJson);
} else {
Log.d("MainActivity", "Keep existing pieces.json");
}
// Check stock.json and stock.csv output files to avoid control server error
checkOrCreateEmptyFile(new File(rootDir, Config.OUTPUT_JSON_NAME), "[]");
checkOrCreateEmptyFile(new File(rootDir, Config.OUTPUT_CSV_NAME), "");
// Check images folder
File imgDir = new File(rootDir, Config.IMAGES_SUBDIR_NAME);
if (!imgDir.exists()) {
if (!imgDir.mkdirs()) {
Log.e("MainActivity", "Can't create images dir.");
return;
}
}
// Copy images only if not found
String[] filesInImgDir = imgDir.list();
if (filesInImgDir == null || filesInImgDir.length == 0) {
Log.i("MainActivity", "Images folder empty. Installing images...");
copyFolderFromAssets(Config.IMAGES_SUBDIR_NAME, imgDir);
} else {
Log.d("MainActivity", "Keep existing images.");
}
}
/**
* Create a file with default content if not found
*/
private void checkOrCreateEmptyFile(File file, String defaultContent) {
if (!file.exists()) {
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(defaultContent.getBytes());
Log.d(TAG, "Initialisation de : " + file.getName());
} catch (IOException e) {
Log.e(TAG, "Erreur lors de l'initialisation de " + file.getName(), e);
}
} }
} }
/**
* Copy folder from assets
*/
private void copyFolderFromAssets(String assetDirName, File destDir) { private void copyFolderFromAssets(String assetDirName, File destDir) {
try { try {
String[] files = getAssets().list(assetDirName); String[] files = getAssets().list(assetDirName);
@@ -280,25 +368,50 @@ public class MainActivity extends AppCompatActivity {
} }
} }
} catch (IOException e) { } catch (IOException e) {
Log.e("MainActivity", "Erreur listing assets: " + assetDirName, e); Log.e("MainActivity", "Listing assets error: " + assetDirName, e);
} }
} }
/**
* Copy file from assets
*/
private void copyFileFromAssets(String assetName, File destFile) { private void copyFileFromAssets(String assetName, File destFile) {
// Optimized read // Optimized read
// Try-with-resources ensures streams are automatically closed, avoid memory leaks // Try-with-resources ensures streams are automatically closed, avoid memory leaks
try (InputStream in = getAssets().open(assetName); try (InputStream in = getAssets().open(assetName);
OutputStream out = new FileOutputStream(destFile)) { OutputStream out = new FileOutputStream(destFile)) {
byte[] buffer = new byte[1024]; byte[] buffer = new byte[8192];
int read; int read;
while ((read = in.read(buffer)) != -1) { while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read); out.write(buffer, 0, read);
} }
Log.d("MainActivity", "Succès : " + assetName + " copié."); Log.d("MainActivity", "Success : " + assetName + " copied.");
} catch (IOException e) { } catch (IOException e) {
Log.e("MainActivity", "Erreur copie asset: " + assetName, e); Log.e("MainActivity", "Can't copy asset: " + assetName, e);
}
}
/**
* Get device IP to print it to users for easy remote control
*/
private String getDeviceIP() {
try {
java.util.Enumeration<java.net.NetworkInterface> interfaces = java.net.NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
java.net.NetworkInterface iface = interfaces.nextElement();
java.util.Enumeration<java.net.InetAddress> addresses = iface.getInetAddresses();
while (addresses.hasMoreElements()) {
java.net.InetAddress addr = addresses.nextElement();
if (!addr.isLoopbackAddress() && addr instanceof java.net.Inet4Address) {
return addr.getHostAddress();
} }
} }
} }
} catch (Exception e) {
Log.e(TAG, "IP error", e);
}
return "127.0.0.1";
}
}

View File

@@ -1,12 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- activity_main.xml --> <!-- activity_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="horizontal" android:orientation="horizontal"
android:background="#ffffff"> android:background="#ffffff"
android:baselineAligned="false"> <FrameLayout
<FrameLayout
android:id="@+id/contentFrame" android:id="@+id/contentFrame"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="match_parent"
@@ -17,9 +18,9 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingLeft="20dp"> android:paddingStart="20dp"
android:paddingEnd="20dp"
<LinearLayout tools:ignore="RtlSymmetry"> <LinearLayout
android:id="@+id/categoriesLayout" android:id="@+id/categoriesLayout"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
@@ -31,33 +32,49 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_weight="1" android:layout_weight="1"
android:scaleType="fitCenter" /> android:scaleType="fitCenter"
</LinearLayout> android:contentDescription="@string/app_name" /> </LinearLayout>
<LinearLayout <FrameLayout
android:id="@+id/categoryItemsLayout" android:id="@+id/categoryItemsLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:visibility="gone">
android:visibility="gone"
android:padding="10dp">
<Button
android:id="@+id/backToHomeBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="← Retour" />
<ScrollView <ScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:padding="10dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<GridLayout <GridLayout
android:id="@+id/gridPieces" android:id="@+id/gridPieces"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:columnCount="4" /> android:columnCount="4" />
</ScrollView>
<View
android:layout_width="match_parent"
android:layout_height="100dp" />
</LinearLayout> </LinearLayout>
</ScrollView>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnBackFromCategory"
android:layout_width="200dp"
android:layout_height="70dp"
android:layout_gravity="bottom|start"
android:layout_margin="20dp"
android:text="@string/cart_back_btn"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="#FFFFFF"
app:backgroundTint="#1E88E5"
android:elevation="8dp"
tools:targetApi="l" />
</FrameLayout>
</FrameLayout> </FrameLayout>
<include layout="@layout/partial_cart" /> <include layout="@layout/partial_cart" />

View File

@@ -19,13 +19,15 @@
<TextView <TextView
android:id="@+id/itemName" android:id="@+id/itemName"
android:layout_width="wrap_content" android:layout_width="150dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:text="Item name" tools:text="Item name"
android:textSize="20sp" android:textSize="20sp"
android:layout_marginTop="0dp" android:layout_marginTop="0dp"
android:lines="1" android:minLines="2"
android:ellipsize="end" /> android:maxLines="2"
android:ellipsize="end"
android:gravity="center"/>
<TextView <TextView
android:id="@+id/itemPrice" android:id="@+id/itemPrice"

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- partial_panier.xml --> <!-- partial_panier.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/cartContainer" android:id="@+id/cartContainer"
android:layout_width="350dp" android:layout_width="350dp"
android:layout_height="match_parent" android:layout_height="match_parent"
@@ -56,29 +57,29 @@
android:orientation="horizontal" android:orientation="horizontal"
android:weightSum="2"> android:weightSum="2">
<Button <androidx.appcompat.widget.AppCompatButton
android:id="@+id/clearCartBtn" android:id="@+id/clearCartBtn"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_weight="1" android:layout_weight="1"
android:layout_height="60dp" android:layout_height="70dp"
android:text="@string/cart_empty_btn" android:text="@string/cart_empty_btn"
android:textSize="18sp" android:textSize="18sp"
android:textStyle="bold"
android:textColor="#FFFFFF" android:textColor="#FFFFFF"
android:background="#E53935" app:backgroundTint="#E53935"
android:layout_marginEnd="5dp" android:layout_marginEnd="5dp"/>
android:onClick="emptyCart"/>
<Button <androidx.appcompat.widget.AppCompatButton
android:id="@+id/validateCartBtn" android:id="@+id/validateCartBtn"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_weight="1" android:layout_weight="1"
android:layout_height="60dp" android:layout_height="70dp"
android:text="@string/cart_validate_btn" android:text="@string/cart_validate_btn"
android:textSize="18sp" android:textSize="18sp"
android:textStyle="bold"
android:textColor="#FFFFFF" android:textColor="#FFFFFF"
android:background="#43A047" app:backgroundTint="#43A047"
android:layout_marginStart="5dp" android:layout_marginStart="5dp"/>
android:onClick="validateCart"/>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -2,9 +2,9 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.Pignon" parent="Theme.AppCompat.Light.DarkActionBar"> <style name="Theme.Pignon" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="colorPrimary">@color/purple_500</item> <item name="colorPrimary">@color/pignon_blue</item>
<item name="colorPrimaryDark">@color/purple_700</item> <item name="colorPrimaryDark">@color/pignon_blue_dark</item>
<item name="colorAccent">@color/teal_200</item> <item name="colorAccent">@color/pignon_blue</item>
</style> </style>
</resources> </resources>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="purple_500">#FF6200EE</color> <color name="pignon_blue">#0049AF</color>
<color name="purple_700">#FF3700B3</color> <color name="pignon_blue_dark">#003580</color>
<color name="teal_200">#FF03DAC5</color> <color name="white">#FFFFFF</color>
</resources> </resources>

View File

@@ -3,8 +3,9 @@
<string name="cart_name">Mes sacoches</string> <string name="cart_name">Mes sacoches</string>
<string name="cart_validate_btn">Valider</string> <string name="cart_validate_btn">Valider</string>
<string name="cart_back_btn">Retour</string>
<string name="cart_empty_btn">Vider</string> <string name="cart_empty_btn">Vider</string>
<string name="cart_empty">Sacoches vides</string> <string name="cart_empty">C\'est vide !</string>
<string name="cart_item">Quantité : %1$d (%2$d - %3$d €)</string> <string name="cart_item">Quantité : %1$d (%2$d - %3$d €)</string>
<string name="currency"></string> <string name="currency"></string>
@@ -13,6 +14,6 @@
<string name="popup_name">Mes sacoches</string> <string name="popup_name">Mes sacoches</string>
<string name="popup_item"><![CDATA[<b>%1$d x %2$s</b> : %3$d - %4$d €<br>]]></string> <string name="popup_item"><![CDATA[<b>%1$d x %2$s</b> : %3$d - %4$d €<br>]]></string>
<string name="popup_total"><![CDATA[<br><b>Contribution consciente dans la petite boîte : %1$d - %2$d €</b>]]></string> <string name="popup_total"><![CDATA[<br><b>Contribution consciente dans la petite boîte : %1$d - %2$d €</b>]]></string>
<string name="popup_end">Sacoches sauvegardées, merci !</string> <string name="popup_end">Sacoches sauvegardées, merci ! ❤️</string>
</resources> </resources>

View File

@@ -2,11 +2,9 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.Pignon" parent="Theme.AppCompat.Light.DarkActionBar"> <style name="Theme.Pignon" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="colorPrimary">@color/purple_500</item> <item name="colorPrimary">@color/pignon_blue</item>
<item name="colorPrimaryDark">@color/purple_700</item> <item name="colorPrimaryDark">@color/pignon_blue_dark</item>
<item name="colorAccent">@color/teal_200</item> <item name="colorAccent">@color/pignon_blue</item>
<item name="actionBarSize">40dp</item>
</style> </style>
</resources> </resources>