Add PostgreSQL backend and config
This commit is contained in:
102
app.js
102
app.js
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user