### Corrections
* Correction de fautes de syntaxe.
* Correction d'un bug qui empêchait d'utiliser une recette.

### Ajouts
* Traduction complète de l'application.
* Amélioration des recherches.
* Ajout d'une confirmation avant l'utilisation d'un mélange ou d'une recette.
* Le nom des mélanges peut maintenant être modifié.
* Le bouton retour envoie maintenant vers la dernière page visitée.
This commit is contained in:
FyloZ 2019-12-27 22:26:32 -05:00
parent f21cfd94c1
commit 6fb6a4829c
43 changed files with 364 additions and 353 deletions

View File

@ -10,7 +10,7 @@
</parent>
<groupId>dev.fyloz.trial.colorrecipesexplorer</groupId>
<artifactId>ColorRecipesExplorer</artifactId>
<version>1.1.2</version>
<version>1.1.3</version>
<name>Color Recipes Explorer</name>
<properties>

View File

@ -23,6 +23,7 @@ public abstract class ResponseBuilder<T extends ResponseBuilder, R> {
// Ajoute l'URL de base à toutes les réponses
attributes.put("baseUrl", ControllerUtils.getCurrentBaseUrl());
attributes.put("referer", ControllerUtils.getLatestUrl());
}
protected abstract void addResponseCodeToAttribute(String responseCodeType, String responseMessagePath, String[] parameters);

View File

@ -104,11 +104,8 @@ public class Material extends BeanModel implements Serializable {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Material material = (Material) o;
return Float.compare(material.inventoryQuantity, inventoryQuantity) == 0 &&
isMixType == material.isMixType &&
Objects.equals(materialID, material.materialID) &&
Objects.equals(materialCode, material.materialCode) &&
Objects.equals(materialType, material.materialType);
return Objects.equals(materialID, material.materialID) &&
Objects.equals(materialCode, material.materialCode);
}
@Override

View File

@ -101,8 +101,7 @@ public class MaterialType extends BeanModel implements Serializable {
MaterialType that = (MaterialType) o;
return Objects.equals(materialTypeID, that.materialTypeID) &&
Objects.equals(materialTypeName, that.materialTypeName) &&
Objects.equals(prefix, that.prefix) &&
Objects.equals(usePercentages, that.usePercentages);
Objects.equals(prefix, that.prefix);
}
@Override

View File

@ -6,7 +6,6 @@ import dev.fyloz.trial.colorrecipesexplorer.core.model.MixQuantity;
import dev.fyloz.trial.colorrecipesexplorer.core.services.model.MaterialService;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@ -22,7 +21,7 @@ public class InventoryService {
public String checkQuantities(Mix mix, Map<Material, Float> quantities) {
for (Material material : mix.getMixQuantities().stream().map(MixQuantity::getMaterial).collect(Collectors.toList())) {
if (!material.isMixType()) {
float quantity = quantities.get(material);
Float quantity = quantities.get(material);
if (quantity > material.getInventoryQuantity()) {
return String.format("%s-%s", mix.getMixID(), material.getMaterialID());
@ -34,12 +33,11 @@ public class InventoryService {
}
public boolean useMix(Mix mix, Map<Material, Float> quantities) {
List<Material> materials = mix.getMixQuantities().stream().map(MixQuantity::getMaterial).collect(Collectors.toList());
for (Material material : materials) {
if (!material.isMixType()) {
float quantity = quantities.get(material);
for (Map.Entry<Material, Float> entry : quantities.entrySet()) {
Material material = entry.getKey();
material.setInventoryQuantity(material.getInventoryQuantity() - quantity);
if (!material.isMixType()) {
material.setInventoryQuantity(material.getInventoryQuantity() - entry.getValue());
if (!materialService.update(material).isPresent()) return false;
}
}

View File

@ -12,6 +12,7 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@ -37,6 +38,12 @@ public class MaterialService extends GenericService<Material, MaterialDao> {
return dao.findAllByMaterialType(materialType);
}
public List<Material> getAllOrdered() {
return getAll().stream()
.sorted(Comparator.comparing(Material::getMaterialCode))
.collect(Collectors.toList());
}
/**
* Vérifie si un produit est lié à un ou plusieurs mélanges.
*

View File

@ -86,6 +86,8 @@ public class MixService extends GenericService<Mix, MixDao> {
materials.add(found.get());
}
mix.getMixType().setTypeName(formDto.getMixTypeName());
List<MixQuantity> mixQuantities = createMixQuantities(mix, materials, formDto.getQuantities());
// Supprime les anciens MixQuantity pour éviter les doublons et les entrées inutiles dans la base de données

View File

@ -5,6 +5,8 @@ import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.net.URI;
import java.net.URISyntaxException;
public class ControllerUtils {
@ -12,15 +14,44 @@ public class ControllerUtils {
return String.format("redirect:/%s", viewName);
}
public static URI getUri(HttpServletRequest request) throws URISyntaxException {
return new URI(request.getRequestURL().toString());
}
public static String getUrlFromURI(URI uri) {
String scheme = uri.getScheme();
String host = uri.getHost();
int port = uri.getPort();
return String.format("%s://%s:%s", scheme, host, port);
}
public static String getCurrentBaseUrl() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return null;
}
if (attributes == null) return "";
HttpServletRequest request = attributes.getRequest();
String port = ":" + (ColorRecipesExplorerApplication.USE_PORT ? request.getServerPort() : "");
return String.format("%s://%s%s%s", request.getScheme(), request.getServerName(), port, request.getContextPath());
}
public static String getLatestUrl() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) return "";
HttpServletRequest request = attributes.getRequest();
try {
String currentDomainName = getUrlFromURI(getUri(request));
String referer = request.getHeader("referer");
if (referer == null) return currentDomainName;
String refererURL = getUrlFromURI(new URI(referer));
return refererURL.equals(currentDomainName) ? referer : currentDomainName;
} catch (URISyntaxException e) {
return "";
}
}
}

View File

@ -2,27 +2,6 @@ package dev.fyloz.trial.colorrecipesexplorer.web;
public class StringBank {
public static final String SUCCESS_USING_MATERIALS = "Les quantités de chaque produits utilisés ont été déduites de l'inventaire";
public static final String SUCCESS_SAVING_RECIPE_INFORMATIONS = "Les informations de la recette ont été sauvegardées";
public static final String ERROR_SAVING = "Une erreur est survenue lors de l'enregistrement";
public static final String ERROR_SAVING_IMAGE = "Une erreur est survenue lors de l'enregistrement de l'image";
public static final String ERROR_SAVING_SIMDUT = "Une erreur est survenue lors de l'enregistrement du fichier SIMDUT";
public static final String AUTH_ERROR = "Votre mot de passe n'est pas valide";
// À formatter
public static final String RECIPE_NOT_FOUND = "Aucune recette ayant l'identifiant '%s' n'a été trouvée";
public static final String MIX_NOT_FOUND = "Aucun mélange ayant l'identifiant '%s' n'a été trouvé";
public static final String MATERIAL_NOT_FOUND = "Aucun produit ayant l'identifiant '%s' n'a été trouvé";
public static final String MATERIAL_ALREADY_EXIST = "Il y a déjà un produit s'appellant '%s'";
public static final String MATERIAL_TYPE_ALREADY_EXIST = "Il y a déjà un type de produit s'appellant '%s'";
public static final String COMPANY_NOT_FOUND = "Aucune bannière ayant l'identifiant '%s' n'a été trouvée";
public static final String COMPANY_ALREADY_EXIST = "Il y a déjà une bannière s'appellant '%s'";
public static final String MATERIAL_LINKED = "Le produit '%s' est lié à une ou plusieurs recettes, veuillez les supprimer d'abord";
public static final String COMPANY_LINKED = "La bannière '%s' est liée à une ou plusieurs recettes, veuillez les supprimer d'abord";
public static final String MIX_NOT_ASSOCIATED_WITH_RECIPE = "Le mélange ayant l'identifiant '%s' n'est pas associé à la recette ayant l'identifiant '%s'";
public static final String NOT_ENOUGH_MATERIAL = "Il n'y a pas assez de '%s' en inventaire pour cette recette";
public static final String MIX_TYPE_ALREADY_USED = "Cette recette contient déjà un mélange du type '%s'";
// Types de réponse
public static final String RESPONSE_ERROR = "error";
public static final String RESPONSE_SUCCESS = "success";

View File

@ -44,7 +44,7 @@ public class InventoryController {
public ModelAndView getInventory(ModelAndView model) {
return new ModelResponseBuilder(model)
.withView(INVENTORY)
.addResponseData(ResponseDataType.MATERIALS, materialService.getAll().stream().filter(m -> !m.isMixType()).collect(Collectors.toList()))
.addResponseData(ResponseDataType.MATERIALS, materialService.getAllOrdered().stream().filter(m -> !m.isMixType()).collect(Collectors.toList()))
.addResponseData(ResponseDataType.MATERIAL_TYPES, materialTypeService.getAll())
.build();
}
@ -76,7 +76,7 @@ public class InventoryController {
@PostMapping(value = USE_INVENTORY, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
@Transactional
// TODO traduit les méthodes JSON
// TODO vers DTO
public Map<String, Object> consumeMaterials(@RequestBody Map<String, Object> form) {
JSONResponseBuilder responseBuilder = new JSONResponseBuilder();

View File

@ -27,7 +27,7 @@ mix.location=Location
material.code=Code
material.inventoryQuantity=Inventory quantity
material.type=Material type
material.SIMDUTFile=SIMDUT File
material.simdutFile=SIMDUT File
units.milliliters=Milliliters
units.liters=Liters
units.gallons=Gallons
@ -94,3 +94,9 @@ recipe.error.anyFound=No recipes were found.
recipe.exportAllXLS=Export all colors (XLSX)
recipe.xlsVersion=XLSX version
keyword.updates=Updates history
material.simdutFile.notFound=No SIMDUT file found
recipe.warning.changesNotSaved=Changes are not saved
recipe.warning.exportAll=Do you really want to export all the colors? This can take some times and slow down the application.
warning.noResult=Nothing corresponding the the research was found
inventory.askUseMix=Do you really want to deduct this mix from the inventory?
inventory.askUseRecipe=Do you really want to deduct this recipe from the inventory?

View File

@ -27,7 +27,7 @@ mix.location=Position
material.code=Code
material.inventoryQuantity=Quantité en inventaire
material.type=Type de produit
material.SIMDUTFile=Fichier SIMDUT
material.simdutFile=Fichier SIMDUT
units.milliliters=Millilitres
units.liters=Litres
units.gallons=Gallons
@ -94,4 +94,10 @@ recipe.error.anyFound=Aucune recette n'a été trouvée.
recipe.exportAllXLS=Exporter toutes les couleurs (XLSX)
recipe.xlsVersion=Version XLSX
keyword.updates=Historique des mises à jour
material.simdutFile.notFound=Aucun fichier SIMDUT trouvé
recipe.warning.changesNotSaved=Des modifications ne sont pas sauvegardées
recipe.warning.exportAll=Voulez-vous vraiment exporter toutes les couleurs? Cela peut prendre un certain temps et ralentir l'application.
warning.noResult=Rien correspondant à la recherche n'a été trouvé
inventory.askUseMix=Êtes-vous certain de vouloir déduire ce mélange de l'inventaire?
inventory.askUseRecipe=Êtes-vous certain de vouloir déduire cette recette de l'inventaire?

View File

@ -119,6 +119,8 @@ textarea {
border-style: solid;
border-color: #7a7a7a;
border-width: 1px;
margin-left: 20px;
padding: 10px;
}
.dropdown {
@ -201,12 +203,14 @@ nav a:hover, .dropdown:hover .dropbtn {
}
#researchBoxContainer {
text-align: right;
float: right;
}
#researchBox {
margin-top: 10px;
margin-right: 50px;
margin-left: -200px;
width: 150px;
}
.errorBox {

View File

@ -21,7 +21,7 @@ const successMsgBoxText = successMsgBox.querySelector("p");
.catch(err => {
if (err.response.status === 404) {
e.parentElement.classList.add("nosimdut");
e.parentElement.title = "Aucun fichier SIMDUT trouvé";
e.parentElement.title = simdutNotFoundText;
}
});
});
@ -33,9 +33,9 @@ const successMsgBoxText = successMsgBox.querySelector("p");
window.addEventListener("load", () => {
document.querySelectorAll(".returnIndex").forEach((e) => {
document.querySelectorAll(".returnButton").forEach((e) => {
e.addEventListener("click", () => {
document.location.href = "/";
document.location.href = referer;
});
});
@ -84,11 +84,10 @@ window.addEventListener("load", () => {
});
});
window.addEventListener("change", e => {
window.addEventListener("keyup", e => {
if (e.target) {
if (e.target.classList.contains("toSave")) {
// TODO traductions
warningMsgBoxText.innerText = "Des modifications ne sont pas été sauvegardées";
warningMsgBoxText.innerText = changesNotSavedText;
showElement(warningMsgBox);
}
}
@ -102,13 +101,13 @@ window.addEventListener("load", () => {
});
function askDatabaseExport() {
return confirm("Voulez-vous vraiment exporter toutes les couleurs? Cela peut prendre un certain temps et ralentir l'application pendant un certain temps.");
return confirm(exportAllWarningText);
}
function checkPassword(form, callback) {
hideElement(errorMsgBox);
const password = prompt("Quel est votre mot de passe?");
const password = prompt(askPasswordText);
let data = {};
data.password = password;
@ -120,13 +119,13 @@ function checkPassword(form, callback) {
if (callback != null) callback();
return true;
} else {
errorMsgBoxText.innerText = "Votre mot de passe n'est pas valide";
errorMsgBoxText.innerText = invalidPasswordText;
showElement(errorMsgBox);
return false;
}
})
.catch(e => {
errorMsgBoxText.innerText = "Une erreur est survenue lors de l'envoie des informations vers le serveur.";
errorMsgBoxText.innerText = generalErrorText;
showElement(errorMsgBox);
console.log(e);
return false;
@ -185,3 +184,7 @@ function round(x) {
function percentageOf(percentage, number) {
return (percentage / 100) * number;
}
function searchIn(searchString, str) {
return str.toUpperCase().indexOf(searchString.toUpperCase()) > -1;
}

View File

@ -16,7 +16,7 @@ window.addEventListener("load", () => {
init();
})
.catch(e => {
errorMsgBoxText.innerText = "Une erreur est survenue lors de la récupération des produits";
errorMsgBoxText.innerText = generalErrorText;
showElement(errorMsgBox);
console.log(e);
});
@ -149,12 +149,10 @@ function searchMaterial(input) {
let filter, filterUpper, materials;
filter = input.value;
filterUpper = filter.toUpperCase();
materials = input.parentElement.querySelectorAll(".materialList p");
materials.forEach(e => {
if (e.innerText.toUpperCase().indexOf(filterUpper) > -1 ||
e.dataset.materialtype.toUpperCase().indexOf(filterUpper) > -1) e.style.display = "";
if (searchIn(filter, e.textContent) || searchIn(filter, e.dataset.materialType)) e.style.display = "";
else e.style.display = "none";
});

View File

@ -13,12 +13,11 @@
<section>
<h1 th:text="#{company.add.title}"></h1>
<p th:text="#{company.success.created(${companyName})}"></p>
<button class="returnIndex" th:text="#{keyword.back}"></button>
<button class="returnButton" th:text="#{keyword.back}"></button>
</section>
<!-- Fragment du pied de page -->
<footer th:include="fragments.html :: footer(null, true)"></footer>
<script src="/js/main.js"></script>
</body>
</html>
</html>

View File

@ -23,14 +23,7 @@
th:text="#{company.form.companyName} + ':'"></label></b></td>
<td><input type="text" th:field="*{companyName}" required /></td>
</tr>
<tr>
<td>
<button class="returnIndex" type="button" th:text="#{keyword.back}"></button>
</td>
<td>
<button type="submit" th:text="#{keyword.save}"></button>
</td>
</tr>
<tr th:include="fragments.html :: mainTableButtons"></tr>
</table>
</form>
</div>
@ -39,6 +32,5 @@
<footer th:include="fragments.html :: footer(null, true)"></footer>
<script src="/js/main.js"></script>
</body>
</html>

View File

@ -59,6 +59,5 @@
<footer th:include="fragments.html :: footer('/company/remover', true)"></footer>
<script src="/js/main.js"></script>
</body>
</html>

View File

@ -15,7 +15,7 @@
<!-- Fragment du pied de page -->
<footer th:include="fragments.html :: footer(null, false)"></footer>
<script src="/js/main.js"></script>
<script>
/*<![CDATA[*/
(() => {
@ -26,4 +26,4 @@
/*]]>*/
</script>
</body>
</html>
</html>

View File

@ -104,6 +104,30 @@
<a th:unless="${link == null}" th:href="@{${link + '?lang=__${#locale.toString() == 'en' ? 'fr' : 'en'}__'}}"
th:text="#{footer.lang}"></a>
</th:block>
<script src="/js/main.js"></script>
<script>
/*<![CDATA[*/
const simdutNotFoundText = "[[#{material.simdutFile.notFound}]]".replace("&#39;", "'");
const changesNotSavedText = "[[#{recipe.warning.changesNotSaved}]]".replace("&#39;", "'");
const exportAllWarningText = "[[#{recipe.warning.exportAll}]]".replace("&#39;", "'");
const askPasswordText = "[[#{password.ask}]]".replace("&#39;", "'");
const invalidPasswordText = "[[#{password.notValid}]]".replace("&#39;", "'");
const generalErrorText = "[[#{error.serverError}]]".replace("&#39;", "'");
const researchNotFound = "[[#{warning.noResult}]]".replace("&#39;", "'");
const referer = "[[${referer}]]";
/*]]>*/
</script>
</div>
<div th:fragment="mainTableButtons">
<td class="centerTd">
<button class="returnButton" type="button" th:text="#{keyword.back}"></button>
</td>
<td class="centerTd">
<button type="submit" th:text="#{keyword.save}"></button>
</td>
</div>
</body>

View File

@ -23,6 +23,5 @@
<footer th:include="fragments.html :: footer(null, true)"></footer>
<script src="/js/main.js"></script>
</body>
</html>

View File

@ -48,7 +48,7 @@
<!-- Corps de la page -->
<section>
<div id="researchBoxContainer">
<input id="researchBox" type="text" th:placeholder="#{keyword.search}" onchange="performSearch(this.value)"/>
<input id="researchBox" type="text" th:placeholder="#{keyword.search}" onkeyup="performSearch(this.value)"/>
</div>
<div th:include="fragments.html :: messages"></div>
@ -85,10 +85,8 @@
<!-- Fragment du pied de page -->
<footer th:include="fragments.html :: footer(null, true)"></footer>
<script src="/js/main.js"></script>
<script th:inline="javascript">
/*<![CDATA[*/
(() => {
document.querySelectorAll(".gotoRecipe").forEach(e => {
e.addEventListener("click", () => {
@ -103,62 +101,73 @@
if (approbationDate === null) {
e.classList.add("unapproved");
e.title = /*[[#{recipe.warning.notApproved}]]*/ "Cette recette n'est pas approuvée";
e.title = [[#{recipe.warning.notApproved}]];
}
});
})();
const companyRows = document.querySelectorAll(".companyTab");
function performSearch(searchString) {
if (!searchString || !searchString.replace(/\s/g, '').length) {
document.querySelectorAll(".companyTab").forEach(c => {
c.classList.add("researchResult");
let found = false;
let emptySearch = false;
c.querySelectorAll(".recipeRow").forEach(r => {
r.classList.add("researchResult");
});
});
closeTabs();
return;
const recipesContainer = document.querySelector(".recipesContainer:not(.researchEnabled)");
if (recipesContainer !== undefined && recipesContainer !== null) {
recipesContainer.classList.add("researchEnabled");
}
hideElement(errorMsgBox);
axios.get(`/search?searchString=${searchString}`)
.then(r => {
const result = r.data.result;
document.querySelectorAll(".researchResult").forEach(t => {
t.classList.remove("researchResult");
});
const recipesContainer = document.querySelector(".recipesContainer:not(.researchEnabled)");
if (recipesContainer !== undefined && recipesContainer !== null) {
recipesContainer.classList.add("researchEnabled");
companyRows.forEach(c => {
const recipeRows = c.querySelectorAll(".recipeRow");
hideElement(warningMsgBox);
if (!searchString || !searchString.replace(/\s/g, '').length) {
c.classList.add("researchResult");
recipeRows.forEach(r => {
r.classList.add("researchResult");
});
found = true;
emptySearch = true;
} else if (searchIn(searchString, c.querySelector("h1").dataset.companyname)) {
c.classList.add("researchResult");
recipeRows.forEach(r => r.classList.add("researchResult"));
found = true;
} else {
recipeRows.forEach(r => {
r.querySelectorAll(".descriptionCell").forEach(d => {
if (searchIn(searchString, d.textContent)) {
r.classList.add("researchResult");
c.classList.add("researchResult");
found = true;
}
});
});
}
document.querySelectorAll(".researchResult").forEach(t => {
t.classList.remove("researchResult");
});
if (!found) {
warningMsgBoxText.innerText = researchNotFound;
showElement(warningMsgBox);
}
Object.keys(result).forEach(c => {
const companyTab = document.querySelector(`.companyTab[data-companyID=\"${c}\"]`);
companyTab.classList.add("researchResult");
companyTab.querySelector(".recipesList").style.display = "table";
for (let r = 0; r < result[c].length; r++) {
const recipeRow = companyTab.querySelector(`.recipeRow[data-recipeID=\"${result[c][r]}\"]`);
recipeRow.classList.add("researchResult");
}
});
})
.catch(e => {
console.log(e);
errorMsgBoxText.innerText = /*[[#{error.serverError}]]*/ "Erreur";
showElement(errorMsgBox);
});
if (emptySearch) closeTabs();
else openTabs();
}
);
}
function closeTabs() {
document.querySelectorAll(".recipesList").forEach(l => {
l.style.display = "none";
});
document.querySelectorAll(".recipesList").forEach(l => l.style.display = "none");
}
function openTabs() {
document.querySelectorAll(".recipesList").forEach(l => l.style.display = "");
}
/*]]>*/

View File

@ -55,10 +55,11 @@
th:if="${materialType.materialTypeName.equalsIgnoreCase('aucun')}">
<input id="anyTypeId" th:value="${materialType.materialTypeID}" type="hidden"/>
</th:block>
<h1 th:text="#{menu.inventory}"></h1>
<div id="researchBoxContainer">
<input type="text" id="researchBox" th:placeholder="#{keyword.search}" onchange="performSearch(this.value)"/>
<input type="text" id="researchBox" th:placeholder="#{keyword.search}" onkeyup="performSearch(this.value)"/>
</div>
<div th:include="fragments.html :: messages"></div>
@ -190,45 +191,46 @@
});
}
function performSearch(searchString) {
if (!searchString || !searchString.replace(/\s/g, '').length) {
document.querySelectorAll(".materialRow").forEach(m => {
m.classList.add("researchResult");
});
const materialRows = document.querySelectorAll(".materialRow");
return;
function performSearch(searchString) {
hideElement(warningMsgBox);
let found = false;
const recipesContainer = document.querySelector(".materialsContainer:not(.researchEnabled)");
if (recipesContainer !== undefined && recipesContainer !== null) {
recipesContainer.classList.add("researchEnabled");
}
hideElement(errorMsgBox);
axios.get(`inventory/search?searchString=${searchString}`)
.then(r => {
const result = r.data.result;
document.querySelectorAll(".researchResult").forEach(t => {
t.classList.remove("researchResult");
});
const recipesContainer = document.querySelector(".materialsContainer:not(.researchEnabled)");
if (recipesContainer !== undefined && recipesContainer !== null) {
recipesContainer.classList.add("researchEnabled");
materialRows.forEach(row => {
if (!searchString || !searchString.replace(/\s/g, '').length) {
row.classList.add("researchResult");
found = true;
} else {
const materialCode = row.querySelector(".materialCode").textContent;
const materialType = row.querySelector(".materialType").textContent;
if (searchIn(searchString, materialCode) || searchIn(searchString, materialType)) {
row.classList.add("researchResult");
found = true;
}
}
});
document.querySelectorAll(".researchResult").forEach(t => {
t.classList.remove("researchResult");
});
result.forEach(m => {
console.log(`.materialRow[data-materialID=\"${m}\"]`);
const materialRow = document.querySelector(`.materialRow[data-materialID=\"${m}\"]`);
materialRow.classList.add("researchResult");
});
})
.catch(e => {
console.log(e);
errorMsgBoxText.innerText = /*[[#{error.serverError}]]*/ "Erreur";
showElement(errorMsgBox);
});
if (!found) {
warningMsgBoxText.innerText = researchNotFound;
showElement(warningMsgBox);
}
}
/*]]>*/
</script>
<script src="/js/main.js"></script>
</body>
</html>

View File

@ -14,12 +14,11 @@
<p th:text="#{material.success.created(${materialCode})}"></p>
<h1 th:text="#{material.add.title}"></h1>
<button class="returnIndex" th:text="#{keyword.back}"></button>
<button class="returnButton" th:text="#{keyword.back}"></button>
</section>
<!-- Fragment du pied de page -->
<footer th:include="fragments.html :: footer(null, true)"></footer>
<script src="/js/main.js"></script>
</body>
</html>
</html>

View File

@ -59,18 +59,11 @@
</tr>
<tr>
<td><b><label for="simdut" th:text="#{material.SIMDUTFile} + ':'"></label></b></td>
<td><b><label for="simdut" th:text="#{material.simdutFile} + ':'"></label></b></td>
<td><input id="simdut" name="simdut" type="file"/></td>
</tr>
<tr>
<td>
<button class="returnIndex" type="button" th:text="#{keyword.back}"></button>
</td>
<td>
<button type="submit" th:text="#{keyword.save}"></button>
</td>
</tr>
<tr th:include="fragments.html :: mainTableButtons"></tr>
</table>
</form>
</div>
@ -78,7 +71,7 @@
<!-- Fragment du pied de page -->
<footer th:include="fragments.html :: footer(null, true)"></footer>
<script src="/js/main.js"></script>
<script>
function switchUnits(unitSelect) {
const quantityElem = document.querySelector("#quantity");

View File

@ -56,20 +56,13 @@
</tr>
<tr>
<td>
<button id="showSIMDUT" type="button" th:text="#{material.SIMDUTFile}">Fichier SIMDUT</button>
<button id="showSIMDUT" type="button" th:text="#{material.simdutFile}">Fichier SIMDUT</button>
</td>
<td>
<button id="editSIMDUT" type="button" th:text="#{keyword.edit}"></button>
</td>
</tr>
<tr>
<td>
<button class="returnIndex" type="button" th:text="#{keyword.back}"></button>
</td>
<td>
<button type="submit" th:text="#{keyword.save}"></button>
</td>
</tr>
<tr th:include="fragments.html :: mainTableButtons"></tr>
</table>
</form>
</div>
@ -77,7 +70,7 @@
<!-- Fragment du pied de page -->
<footer th:include="fragments.html :: footer(null, true)"></footer>
<script src="/js/main.js"></script>
<script>
(() => {
const materialID = document.querySelector("#materialID").value;

View File

@ -48,7 +48,6 @@
<footer th:include="fragments.html :: footer(null, true)"></footer>
<script src="/js/main.js"></script>
<script>
/*<![CDATA[*/
(() => {

View File

@ -52,6 +52,5 @@
<footer th:include="fragments.html :: footer('/material/remover', true)"></footer>
<script src="/js/main.js"></script>
</body>
</html>

View File

@ -30,6 +30,6 @@
<!-- Fragment du pied de page -->
<footer th:include="fragments.html :: footer(null, true)"></footer>
<script src="/js/main.js"></script>
</body>
</html>

View File

@ -34,14 +34,7 @@
th:text="#{materialType.usePercents} + ':'"></label></b></td>
<td><input type="checkbox" th:field="*{usePercentages}"></td>
</tr>
<tr>
<td>
<button class="returnIndex" type="button" th:text="#{keyword.back}"></button>
</td>
<td>
<button type="submit" th:text="#{keyword.save}"></button>
</td>
</tr>
<tr th:include="fragments.html :: mainTableButtons"></tr>
</table>
</form>
</div>
@ -50,6 +43,5 @@
<footer th:include="fragments.html :: footer(null, true)"></footer>
<script src="/js/main.js"></script>
</body>
</html>

View File

@ -40,14 +40,7 @@
th:text="#{materialType.usePercents} + ':'"></label></b></td>
<td><input type="checkbox" th:field="*{usePercentages}"></td>
</tr>
<tr>
<td>
<button class="returnIndex" type="button" th:text="#{keyword.back}"></button>
</td>
<td>
<button type="submit" th:text="#{keyword.save}"></button>
</td>
</tr>
<tr th:include="fragments.html :: mainTableButtons"></tr>
</table>
</form>
</div>
@ -55,6 +48,6 @@
<!-- Fragment du pied de page -->
<footer th:include="fragments.html :: footer(null, true)"></footer>
<script src="/js/main.js"></script>
</body>
</html>

View File

@ -46,7 +46,6 @@
<footer th:include="fragments.html :: footer(null, true)"></footer>
<script src="/js/main.js"></script>
<script>
/*<![CDATA[*/
(() => {

View File

@ -49,6 +49,6 @@
<!-- Fragment du pied de page -->
<footer th:include="fragments.html :: footer('/materialType/remover', true)"></footer>
<script src="/js/main.js"></script>
</body>
</html>

View File

@ -20,6 +20,8 @@
<section>
<div th:include="fragments.html :: messages"></div>
<h1 th:text="#{mix.add.subtitle(${recipe.recipeCode})}"></h1>
<br/>
<br/>
<div class="form">
<form th:action="@{/mix/creator}" class="requireAuth" method="POST">
@ -45,14 +47,7 @@
</table>
</td>
</tr>
<tr>
<td>
<button class="returnIndex" type="button" th:text="#{keyword.back}"></button>
</td>
<td>
<button type="submit" th:text="#{keyword.save}" th:disabled="${blockButton}"></button>
</td>
</tr>
<tr th:include="fragments.html :: mainTableButtons"></tr>
</table>
</form>
</div>
@ -60,7 +55,7 @@
<!-- Fragment du pied de page -->
<footer th:include="fragments.html :: footer(null, true)"></footer>
<script src="/js/main.js"></script>
<script src="/js/mix.js"></script>
<script>
/*<![CDATA[*/

View File

@ -27,9 +27,15 @@
<form th:action="@{/mix/editor}" class="requireAuth" method="POST">
<!-- Information nécessaire à la création des mélanges -->
<input name="mixID" th:value="${mix.mixID}" type="hidden"/>
<input name="mixTypeName" th:value="${mix.mixType.typeName}" type="hidden"/>
<input name="recipeID" id="recipeID" th:value="${mix.recipe.recipeID}" type="hidden"/>
<label for="mixType" th:text="#{mix.mixType} + ':'"></label>
<input type="text"
id="mixType"
name="mixTypeName"
th:value="${mix.mixType.typeName}"
required/>
<table>
<tr>
<td colspan="2">
@ -44,14 +50,7 @@
</table>
</td>
</tr>
<tr>
<td>
<button class="returnIndex" type="button" th:text="#{keyword.back}"></button>
</td>
<td>
<button type="submit" th:text="#{keyword.save}"></button>
</td>
</tr>
<tr th:include="fragments.html :: mainTableButtons"></tr>
</table>
</form>
</div>
@ -60,7 +59,6 @@
<footer th:include="fragments.html :: footer(null, true)"></footer>
<script src="/js/main.js"></script>
<script src="/js/mix.js"></script>
<script>
/*<![CDATA[*/

View File

@ -20,6 +20,5 @@
<footer th:include="fragments.html :: footer(null, true)"></footer>
<script src="/js/main.js"></script>
</body>
</html>

View File

@ -44,16 +44,9 @@
</tr>
<tr>
<td><b><label th:for="${#ids.next('remark')}" th:text="#{recipe.remark}"></label></b></td>
<td><textarea cols="30" form="recipe-form" rows="10" th:field="*{remark}"></textarea></td>
</tr>
<tr>
<td>
<button class="returnIndex" type="button" th:text="#{keyword.back}"></button>
</td>
<td>
<button type="submit" th:text="#{keyword.save}" th:disabled="${blockButton}"></button>
</td>
<td><textarea cols="20" form="recipe-form" rows="2" th:field="*{remark}"></textarea></td>
</tr>
<tr th:include="fragments.html :: mainTableButtons"></tr>
</table>
</form>
</div>
@ -62,6 +55,5 @@
<footer th:include="fragments.html :: footer(null, true)"></footer>
<script src="/js/main.js"></script>
</body>
</html>

View File

@ -107,13 +107,18 @@
<tr>
<td><b><label th:for="${#ids.next('remark')}" th:text="#{recipe.remark} + ':'"></label></b>
</td>
<td><textarea cols="30" rows="10" th:field="*{remark}"></textarea>
<td><textarea cols="20" rows="2" th:field="*{remark}"></textarea>
</td>
</tr>
</table>
</td>
<td>
<table>
<tr>
<td colspan="2" style="height: 40px; text-align: right">
<button id="newMix" type="button" th:text="#{recipe.edit.addMix}"></button>
</td>
</tr>
<th:block th:each="mix : ${mixes}">
<tr>
<td class="mixNameColumn">
@ -149,13 +154,6 @@
</tr>
<tr th:include="fragments.html :: separator"></tr>
</th:block>
<tr>
<td></td>
<td>
<button id="newMix" type="button" th:text="#{recipe.edit.addMix}"></button>
</td>
</tr>
<tr th:include="fragments.html :: separator"></tr>
</table>
</td>
</tr>
@ -206,14 +204,7 @@
th:text="#{recipe.edit.addImage}"></button>
</td>
</tr>
<tr class="mainTableEndButtons">
<td class="centerTd">
<button class="returnIndex" type="button" th:text="#{keyword.back}"></button>
</td>
<td class="centerTd">
<button type="submit" th:text="#{keyword.save}"></button>
</td>
</tr>
<tr th:include="fragments.html :: mainTableButtons"></tr>
</table>
</form>
</div>
@ -222,7 +213,6 @@
<footer th:include="fragments.html :: footer(null, true)"></footer>
<script src="/js/main.js"></script>
<script>
/*<![CDATA[*/

View File

@ -80,7 +80,6 @@
<footer th:include="fragments.html :: footer(null, true)"></footer>
<script src="/js/main.js"></script>
<script>
(() => {
document.querySelectorAll(".editRecipe").forEach((e) => {

View File

@ -14,10 +14,6 @@
border-collapse: separate;
}
textarea {
padding: 10px;
}
section {
text-align: center;
}
@ -176,8 +172,8 @@
<tr>
<td><b th:text="#{recipe.notice} + ':'"></b></td>
<td>
<textarea class="toSave" cols="30" id="note" name="note" rows="10"
th:text="${recipe.note}"></textarea>
<textarea class="toSave" cols="20" id="note" name="note" rows="2"
th:text="${recipe.note}" style="margin: 0"></textarea>
</td>
</tr>
<tr>
@ -190,103 +186,106 @@
</table>
</td>
<td>
<table style="border-spacing: 20px;">
<th:block th:each="mix : ${mixes}">
<tr>
<td><b th:text="${mix.mixType.typeName} + ':'"></b><br/><br/>
<label
th:for="'location' + ${mix.mixID}"
th:text="' ' + #{mix.location} + ': '">
<!-- Formulaires sans autocomplétion pour éviter que le navigateur cache les valeurs lors d'un rafraichissement et invalide les données dans data-quantityML -->
<form action="#" autocomplete="off">
<table style="border-spacing: 20px;">
<th:block th:each="mix : ${mixes}">
<tr>
<td><b th:text="${mix.mixType.typeName} + ':'"></b><br/><br/>
<label
th:for="'location' + ${mix.mixID}"
th:text="' ' + #{mix.location} + ': '">
</label>
<input
class="recipeLocation toSave"
th:id="'location' + ${mix.mixID}"
name="location"
th:data-mixID="${mix.mixID}"
th:value="${mix.location}"
placeholder="N/A"
type="text"/>
</td>
<td>
<table class="mixes" th:id="'mix-' + ${mix.mixID}">
<tr>
<th th:text="#{keyword.material}"></th>
<th th:text="#{keyword.type}"></th>
<th style="min-width: auto !important;">
<button
type="button"
data-hidden="true"
onclick="hideQuantities(this)">
->
</button>
</th>
<!-- Changement des quantités -->
<th></th>
<th th:text="#{keyword.units}"></th>
<th th:text="#{keyword.calculation}"></th>
</tr>
<!-- Produits -->
<tr th:each="mixQuantity : ${mix.mixQuantities}"
th:with="material = ${mixQuantity.material}"
th:id="'material-' + ${material.materialID}">
<td th:classappend="${material.isMixType()} ? '' : materialCode"
th:data-materialID="${material.materialID}"
th:text="${material.materialCode}"></td>
<td>
<p th:text="${material.materialType.materialTypeName}"></p>
</td>
<td class="inventoryQuantityColumn">
<p class="inventoryQuantity"
th:data-quantityML="${mixQuantity.quantity}"
th:text="${mixQuantity.quantity}"></p>
</td>
<td class="quantityColumn">
<input th:if="${!material.isMixType()}" class="quantityCustomizer"
min="0.001" step="0.001"
th:data-materialID="${material.materialID}"
th:data-mixID="${mix.mixID}"
</label>
<input
class="recipeLocation toSave"
th:id="'location' + ${mix.mixID}"
name="location"
th:data-mixID="${mix.mixID}"
th:value="${mix.location}"
placeholder="N/A"
type="text"/>
</td>
<td>
<table class="mixes" th:id="'mix-' + ${mix.mixID}">
<tr>
<th th:text="#{keyword.material}"></th>
<th th:text="#{keyword.type}"></th>
<th style="min-width: auto !important;">
<button
type="button"
data-hidden="true"
onclick="hideQuantities(this)">
->
</button>
</th>
<!-- Changement des quantités -->
<th></th>
<th th:text="#{keyword.units}"></th>
<th th:text="#{keyword.calculation}"></th>
</tr>
<!-- Produits -->
<tr th:each="mixQuantity : ${mix.mixQuantities}"
th:with="material = ${mixQuantity.material}"
th:id="'material-' + ${material.materialID}">
<td th:classappend="${material.isMixType()} ? '' : materialCode"
th:data-materialID="${material.materialID}"
th:text="${material.materialCode}"></td>
<td>
<p th:text="${material.materialType.materialTypeName}"></p>
</td>
<td class="inventoryQuantityColumn">
<p class="inventoryQuantity"
th:data-quantityML="${mixQuantity.quantity}"
th:data-usePercentages="${material.materialType.usePercentages}"
th:value="${mixQuantity.quantity}"
th:disabled="${material.materialType.usePercentages}"
type="number"/></td>
<td class="unitsColumn">
<p class="inventoryQuantityUnits"
th:unless="${material.materialType.usePercentages}">mL</p>
<p th:if="${material.materialType.usePercentages}">%</p>
</td>
<td class="calculationColumn">
<p class="calculation" th:data-mixID="${mix.mixID}"
th:data-materialID="${material.materialID}"></p>
</td>
</tr>
<tr>
<td class="totalQuantityLabel" colspan="3">
Total:
</td>
<td>
<input
class="totalQuantityCustomizer"
type="number"
min="0.001"
step="0.001"
th:data-mixID="${mix.mixID}"
/>
</td>
<td>
<p class="inventoryQuantityUnits">mL</p>
</td>
</tr>
</table>
</td>
<td>
<button class="useMixSubmit" type="button" th:text="#{keyword.use}"></button>
</td>
</tr>
<tr th:include="fragments.html :: separator"></tr>
</th:block>
</table>
th:text="${mixQuantity.quantity}"></p>
</td>
<td class="quantityColumn">
<input th:if="${!material.isMixType()}" class="quantityCustomizer"
min="0.001" step="0.001"
th:data-materialID="${material.materialID}"
th:data-mixID="${mix.mixID}"
th:data-quantityML="${mixQuantity.quantity}"
th:data-usePercentages="${material.materialType.usePercentages}"
th:value="${mixQuantity.quantity}"
th:disabled="${material.materialType.usePercentages}"
type="number"/></td>
<td class="unitsColumn">
<p class="inventoryQuantityUnits"
th:unless="${material.materialType.usePercentages}">mL</p>
<p th:if="${material.materialType.usePercentages}">%</p>
</td>
<td class="calculationColumn">
<p class="calculation" th:data-mixID="${mix.mixID}"
th:data-materialID="${material.materialID}"></p>
</td>
</tr>
<tr>
<td class="totalQuantityLabel" colspan="3">
Total:
</td>
<td>
<input
class="totalQuantityCustomizer"
type="number"
min="0.001"
step="0.001"
th:data-mixID="${mix.mixID}"
/>
</td>
<td>
<p class="inventoryQuantityUnits">mL</p>
</td>
</tr>
</table>
</td>
<td>
<button class="useMixSubmit" type="button" th:text="#{keyword.use}"></button>
</td>
</tr>
<tr th:include="fragments.html :: separator"></tr>
</th:block>
</table>
</form>
</td>
</tr>
<tr>
@ -315,12 +314,8 @@
<footer th:include="fragments.html :: footer(null, true)"></footer>
<script src="/js/main.js"></script>
<script>
// TODO Le navigateur ne reset pas nécessairement les quantités dans les inputs lors d'un rafraichissement de la page, donc les données dans data-quantityml deviennent fausses.
/*<![CDATA[*/
const quantityKeyword = "[[#{keyword.quantity}]]";
(() => {
@ -338,7 +333,6 @@
e.addEventListener("change", () => {
// Les 3 parentElement récupèrent la table dans laquelle le produit se trouve
// TODO simplifier ?
const parentTable = e.parentElement.parentElement.parentElement;
const firstInput = parentTable.querySelectorAll(".quantityCustomizer")[0];
if (e.dataset.usepercentages === "false") {
@ -410,6 +404,9 @@
});
document.querySelector("#useSubmit").addEventListener("click", () => {
const shouldContinue = confirm("[[#{inventory.askUseRecipe}]]".replace("&#39;", "'"));
if (!shouldContinue) return;
let formData = {};
document.querySelectorAll(".quantityCustomizer").forEach(e => {
@ -430,6 +427,9 @@
document.querySelectorAll(".useMixSubmit").forEach(e => {
e.addEventListener("click", () => {
const shouldContinue = confirm("[[#{inventory.askUseMix}]]".replace("&#39;", "'"));
if (!shouldContinue) return;
let formData = {};
e.parentElement.parentElement.querySelectorAll(".quantityCustomizer").forEach(elem => {

View File

@ -82,6 +82,5 @@
<footer th:include="fragments.html :: footer(null, true)"></footer>
<script src="/js/main.js"></script>
</body>
</html>

View File

@ -25,7 +25,7 @@
<!-- Fragment du pied de page -->
<footer th:include="fragments.html :: footer(null, true)"></footer>
<script src="/js/main.js"></script>
<script>
/*<![CDATA[*/
window.addEventListener("load", e => {
@ -34,7 +34,7 @@
document.querySelector("#markdown").innerHTML = r.data;
})
.catch(e => {
errorMsgBoxText.innerText = "Une erreur est survenue lors de la récupération des mises à jour";
errorMsgBoxText.innerText = generalErrorText;
showElement(errorMsgBox);
console.log(e);
});

View File

@ -1,5 +1,22 @@
# v1.1.3
# v1.2.0 (Imprimante P-touch)
### Corrections
* +++ Correction d'un bug qui empêchait la suppression des mélanges.
### Ajouts
* +++ Ajout du support pour l'imprimante P-touch
* Les produits dans l'inventaire sont maintenant ordonnés alphabétiquement.
# v1.1.3
### Corrections
* Correction de fautes de syntaxe.
* Correction d'un bug qui empêchait d'utiliser une recette.
### Ajouts
* Traduction complète de l'application.
* Amélioration des recherches.
* Ajout d'une confirmation avant l'utilisation d'un mélange ou d'une recette.
* Le nom des mélanges peut maintenant être modifié.
* Le bouton retour envoie maintenant vers la dernière page visitée.
# v1.1.2
### Corrections