Add PostgreSQL backend and config

This commit is contained in:
Matthieu
2025-12-08 11:13:20 +01:00
parent 15515a3512
commit ccdbf19fd8
5 changed files with 537 additions and 10 deletions

102
app.js
View File

@@ -60,6 +60,7 @@
const persistKey = "devis-generator:v1";
const savedListKey = "devis-generator:saved:v1";
const globalSavesEndpoint = "/api/saves";
const save = () => {
try {
localStorage.setItem(persistKey, JSON.stringify(state));
@@ -85,6 +86,35 @@
localStorage.setItem(savedListKey, JSON.stringify(arr));
} catch {}
};
const canUseGlobalSaves = () =>
typeof fetch === "function" && (location?.protocol || "") !== "file:";
const fetchGlobalSaves = async () => {
if (!canUseGlobalSaves()) throw new Error("Sauvegardes globales indisponibles");
const res = await fetch(globalSavesEndpoint, { credentials: "same-origin" });
if (!res.ok) throw new Error("HTTP " + res.status);
const json = await res.json();
if (!Array.isArray(json)) throw new Error("Réponse inattendue");
return json;
};
const pushGlobalSave = async (entry) => {
if (!canUseGlobalSaves()) throw new Error("Sauvegardes globales indisponibles");
const res = await fetch(globalSavesEndpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "same-origin",
body: JSON.stringify(entry),
});
if (!res.ok) throw new Error("HTTP " + res.status);
return res.json();
};
const deleteGlobalSave = async (id) => {
if (!canUseGlobalSaves()) throw new Error("Sauvegardes globales indisponibles");
const res = await fetch(`${globalSavesEndpoint}/${encodeURIComponent(id)}`, {
method: "DELETE",
credentials: "same-origin",
});
if (!res.ok) throw new Error("HTTP " + res.status);
};
const computeQuickTotal = (data) => {
const items = Array.isArray(data.items) ? data.items : [];
const subtotal = items.reduce(
@@ -109,7 +139,7 @@
num || [client, date].filter(Boolean).join(" - ") || "Devis sans titre"
);
};
const saveCurrentQuote = () => {
const saveCurrentQuote = async () => {
const data = buildExportJson();
const input = prompt("Nom du devis", buildDefaultSaveTitle());
if (input === null) return; // Annulé => ne rien faire
@@ -126,14 +156,43 @@
const list = getSavedList();
list.unshift(entry);
setSavedList(list);
alert("Devis enregistré.");
let savedGlobally = false;
try {
if (canUseGlobalSaves()) {
const saved = await pushGlobalSave(entry);
if (saved && typeof saved === "object") {
const cur = getSavedList();
cur[0] = saved; // synchroniser id/horodatage renvoyés
setSavedList(cur);
}
savedGlobally = true;
}
} catch (e) {
console.warn("Sauvegarde globale impossible:", e);
}
alert(
savedGlobally
? "Devis enregistré pour tout le monde (et en local)."
: "Devis enregistré uniquement sur cet appareil."
);
};
const openLibrary = () => {
const openLibrary = async () => {
const modal = document.getElementById("libraryModal");
const listEl = document.getElementById("libraryList");
const emptyEl = document.getElementById("libraryEmpty");
if (!modal || !listEl || !emptyEl) return;
const list = getSavedList();
listEl.innerHTML = `<div class="muted">Chargement...</div>`;
let list = [];
let usedGlobal = false;
if (canUseGlobalSaves()) {
try {
list = await fetchGlobalSaves();
usedGlobal = true;
} catch (e) {
console.warn("Lecture globale impossible, fallback local:", e);
}
}
if (!usedGlobal) list = getSavedList();
listEl.innerHTML = "";
if (!list.length) {
emptyEl.style.display = "";
@@ -161,11 +220,23 @@
const btnDel = document.createElement("button");
btnDel.className = "btn btn-danger";
btnDel.textContent = "Supprimer";
btnDel.addEventListener("click", () => {
btnDel.addEventListener("click", async () => {
if (!confirm("Supprimer ce devis enregistré ?")) return;
const cur = getSavedList().filter((x) => x.id !== e.id);
setSavedList(cur);
openLibrary();
if (usedGlobal && canUseGlobalSaves()) {
try {
await deleteGlobalSave(e.id);
} catch (err) {
alert(
"Suppression impossible côté serveur: " +
(err.message || err)
);
return;
}
} else {
const cur = getSavedList().filter((x) => x.id !== e.id);
setSavedList(cur);
}
await openLibrary();
});
row.appendChild(title);
row.appendChild(price);
@@ -865,9 +936,17 @@
window.print();
});
// Save current quote
$("#saveQuoteBtn")?.addEventListener("click", () => saveCurrentQuote());
$("#saveQuoteBtn")?.addEventListener("click", () =>
saveCurrentQuote().catch((e) =>
alert("Impossible d'enregistrer: " + (e.message || e))
)
);
// Library open/close
$("#openLibraryBtn")?.addEventListener("click", () => openLibrary());
$("#openLibraryBtn")?.addEventListener("click", () =>
openLibrary().catch((e) =>
alert("Impossible d'ouvrir la bibliothèque: " + (e.message || e))
)
);
$("#closeLibraryBtn")?.addEventListener("click", () => closeLibrary());
document
.querySelector("#libraryModal .modal-backdrop")
@@ -1113,6 +1192,9 @@
};
const loadFromJson = (obj) => {
if (obj && obj.data && typeof obj.data === "object") {
obj = obj.data;
}
if (!obj || typeof obj !== "object") throw new Error("JSON invalide");
// Merge top-level simple keys
keysAllowed.forEach((k) => {