fix : exports (csv, xls) fixed, clients expand, fixed
This commit is contained in:
parent
4f82b29f18
commit
5de492b308
|
|
@ -275,88 +275,91 @@ router.post("/export/csv", async (req, res) => {
|
||||||
res.write(headers.join(";") + "\n");
|
res.write(headers.join(";") + "\n");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export par pages de 1000 parcours pour éviter la surcharge mémoire
|
* OPTIMISATION : getFullList pour récupérer tous les parcours en une requête
|
||||||
|
* + batch client pour récupérer tous les clients manquants en une requête
|
||||||
*/
|
*/
|
||||||
const perPage = 1000;
|
|
||||||
let page = 1;
|
// Construction du filtre régions (identique à getParcoursByRegionsPage)
|
||||||
|
let regFilter = "";
|
||||||
|
if (Array.isArray(effective.regions) && effective.regions.length > 0) {
|
||||||
|
const ors = effective.regions.map(r => `dernierUtilisateur.region.nom = "${r}"`);
|
||||||
|
regFilter = `(${ors.join(" || ")})`;
|
||||||
|
}
|
||||||
|
const finalFilter = [regFilter, filter].filter(Boolean).join(" && ");
|
||||||
|
|
||||||
|
// Format avec espaces comme dans le code original qui fonctionnait
|
||||||
|
const expandFields = "contrat, contrat.client, contrat.intermediaire, dernierUtilisateur.region";
|
||||||
|
|
||||||
|
// Récupération de tous les parcours en une seule requête
|
||||||
|
let allParcours;
|
||||||
|
try {
|
||||||
|
allParcours = await parcoursService.getParcoursFullList({
|
||||||
|
filter: finalFilter,
|
||||||
|
sort: sort || "-created",
|
||||||
|
expand: expandFields,
|
||||||
|
batch: 500,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.log("error", "Erreur récupération parcours pour export CSV:", err);
|
||||||
|
if (!res.headersSent) {
|
||||||
|
return res.status(500).send("Erreur lors de la récupération des données");
|
||||||
|
}
|
||||||
|
try { res.end(); } catch {}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
while (true) {
|
// Collecte des IDs clients manquants (l'expand contrat.client ne fonctionne pas en SDK 0.7.x)
|
||||||
|
const missingClientIds = [];
|
||||||
|
for (const parcours of allParcours) {
|
||||||
|
const contrat = parcours["@expand"]?.contrat;
|
||||||
|
if (contrat && contrat.client && !contrat["@expand"]?.client) {
|
||||||
|
missingClientIds.push(contrat.client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupération batch de tous les clients manquants en une seule requête
|
||||||
|
const clientsMap = await clientService.getClientsBatch(missingClientIds);
|
||||||
|
|
||||||
|
// Traitement des parcours
|
||||||
|
for (const parcours of allParcours) {
|
||||||
if (aborted) break;
|
if (aborted) break;
|
||||||
|
|
||||||
let result;
|
const contrat = parcours["@expand"]?.contrat || null;
|
||||||
|
const intermediaire = contrat ? (contrat["@expand"]?.intermediaire || null) : null;
|
||||||
|
|
||||||
|
// Client : d'abord depuis l'expand, sinon depuis le batch
|
||||||
|
let client = contrat ? (contrat["@expand"]?.client || null) : null;
|
||||||
|
if (!client && contrat && contrat.client) {
|
||||||
|
client = clientsMap.get(contrat.client) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastUser = parcours["@expand"]?.dernierUtilisateur;
|
||||||
|
const region = lastUser?.["@expand"]?.region;
|
||||||
|
|
||||||
|
const row = [
|
||||||
|
parcours.numParcours,
|
||||||
|
fmtDateFR(parcours.created),
|
||||||
|
lastUser?.matricule || "NC",
|
||||||
|
lastUser ? `${lastUser.prenom || ""} ${lastUser.nom || ""}`.trim() || "NC" : "NC",
|
||||||
|
region ? (region.nom || "NC") : "NC",
|
||||||
|
contrat ? (contrat.numSaisine || "NC") : "NC",
|
||||||
|
contrat ? (contrat.numContrat || "NC") : "NC",
|
||||||
|
contrat ? (contrat.produit || "NC") : "NC",
|
||||||
|
contrat ? (contrat.type || "NC") : "NC",
|
||||||
|
intermediaire ? (intermediaire.numPortefeuille || "NC") : "NC",
|
||||||
|
intermediaire ? (intermediaire.nom || "NC") : "NC",
|
||||||
|
client ? (client.numClient || "NC") : "NC",
|
||||||
|
client ? (client.nom || "NC") : "NC",
|
||||||
|
];
|
||||||
|
|
||||||
|
const safe = row.map(v => String(v).replaceAll(";", ",").replace(/\r?\n/g, " "));
|
||||||
try {
|
try {
|
||||||
result = await parcoursService.getParcoursByRegionsPage(
|
res.write(safe.join(";") + "\n");
|
||||||
effective.regions,
|
} catch (werr) {
|
||||||
page,
|
logger.log("error", werr);
|
||||||
perPage,
|
aborted = true;
|
||||||
{ filter, sort }
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
logger.log("error", err);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Traitement séquentiel de chaque parcours avec récupération des clients
|
|
||||||
*/
|
|
||||||
for (const parcours of result.items) {
|
|
||||||
if (aborted) break;
|
|
||||||
|
|
||||||
const contrat = parcours["@expand"]?.contrat || null;
|
|
||||||
const intermediaire = contrat ? (contrat["@expand"]?.intermediaire || null) : null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupération du client avec fallback
|
|
||||||
* L'expand PocketBase ne fonctionne pas toujours, donc récupération directe si nécessaire
|
|
||||||
*/
|
|
||||||
let client = contrat ? (contrat["@expand"]?.client || null) : null;
|
|
||||||
if (!client && contrat && contrat.client) {
|
|
||||||
try {
|
|
||||||
const clientData = await clientService.getClient(contrat.client);
|
|
||||||
if (clientData) {
|
|
||||||
client = clientData;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// Erreur silencieuse : client non trouvé ou erreur de récupération
|
|
||||||
client = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const lastUser = parcours["@expand"]?.dernierUtilisateur;
|
|
||||||
const region = lastUser?.["@expand"]?.region;
|
|
||||||
|
|
||||||
const row = [
|
|
||||||
parcours.numParcours,
|
|
||||||
fmtDateFR(parcours.created),
|
|
||||||
lastUser?.matricule || "NC",
|
|
||||||
lastUser ? `${lastUser.prenom || ""} ${lastUser.nom || ""}`.trim() || "NC" : "NC",
|
|
||||||
region ? (region.nom || "NC") : "NC",
|
|
||||||
contrat ? (contrat.numSaisine || "NC") : "NC",
|
|
||||||
contrat ? (contrat.numContrat || "NC") : "NC",
|
|
||||||
contrat ? (contrat.produit || "NC") : "NC",
|
|
||||||
contrat ? (contrat.type || "NC") : "NC",
|
|
||||||
intermediaire ? (intermediaire.numPortefeuille || "NC") : "NC",
|
|
||||||
intermediaire ? (intermediaire.nom || "NC") : "NC",
|
|
||||||
client ? (client.numClient || "NC") : "NC",
|
|
||||||
client ? (client.nom || "NC") : "NC",
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Échappement des caractères spéciaux pour le format CSV
|
|
||||||
* Remplacement des ";" par "," et suppression des retours à la ligne
|
|
||||||
*/
|
|
||||||
const safe = row.map(v => String(v).replaceAll(";", ",").replace(/\r?\n/g, " "));
|
|
||||||
try {
|
|
||||||
res.write(safe.join(";") + "\n");
|
|
||||||
} catch (werr) {
|
|
||||||
logger.log("error", werr);
|
|
||||||
aborted = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Passage à la page suivante si nécessaire
|
|
||||||
if (aborted || page >= result.totalPages) break;
|
|
||||||
page++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!aborted) {
|
if (!aborted) {
|
||||||
|
|
@ -411,14 +414,50 @@ router.post("/export/xls", async (req, res) => {
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export par pages de 1000 parcours pour éviter la surcharge mémoire
|
* OPTIMISATION : getFullList pour récupérer tous les parcours en une requête
|
||||||
|
* + batch client pour récupérer tous les clients manquants en une requête
|
||||||
*/
|
*/
|
||||||
const perPage = 1000;
|
|
||||||
let page = 1;
|
// Construction du filtre régions (identique à getParcoursByRegionsPage)
|
||||||
|
let regFilter = "";
|
||||||
|
if (Array.isArray(effective.regions) && effective.regions.length > 0) {
|
||||||
|
const ors = effective.regions.map(r => `dernierUtilisateur.region.nom = "${r}"`);
|
||||||
|
regFilter = `(${ors.join(" || ")})`;
|
||||||
|
}
|
||||||
|
const finalFilter = [regFilter, filter].filter(Boolean).join(" && ");
|
||||||
|
|
||||||
|
// Format avec espaces comme dans le code original qui fonctionnait
|
||||||
|
const expandFields = "contrat, contrat.client, contrat.intermediaire, dernierUtilisateur.region";
|
||||||
|
|
||||||
|
// Récupération de tous les parcours en une seule requête
|
||||||
|
let allParcours;
|
||||||
|
try {
|
||||||
|
allParcours = await parcoursService.getParcoursFullList({
|
||||||
|
filter: finalFilter,
|
||||||
|
sort: sort || "-created",
|
||||||
|
expand: expandFields,
|
||||||
|
batch: 500,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.log("error", "Erreur récupération parcours pour export XLS:", err);
|
||||||
|
if (!res.headersSent) {
|
||||||
|
return res.status(500).send("Erreur lors de la récupération des données");
|
||||||
|
}
|
||||||
|
try { res.end(); } catch {}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const first = await parcoursService.getParcoursByRegionsPage(
|
// Collecte des IDs clients manquants (l'expand contrat.client ne fonctionne pas en SDK 0.7.x)
|
||||||
effective.regions, page, perPage, { filter, sort }
|
const missingClientIds = [];
|
||||||
);
|
for (const parcours of allParcours) {
|
||||||
|
const contrat = parcours["@expand"]?.contrat;
|
||||||
|
if (contrat && contrat.client && !contrat["@expand"]?.client) {
|
||||||
|
missingClientIds.push(contrat.client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupération batch de tous les clients manquants en une seule requête
|
||||||
|
const clientsMap = await clientService.getClientsBatch(missingClientIds);
|
||||||
|
|
||||||
const fileName = (mode === "full")
|
const fileName = (mode === "full")
|
||||||
? "historique_parcours_complet.xls"
|
? "historique_parcours_complet.xls"
|
||||||
|
|
@ -427,10 +466,7 @@ router.post("/export/xls", async (req, res) => {
|
||||||
res.setHeader("Content-Type", "application/vnd.ms-excel; charset=utf-8");
|
res.setHeader("Content-Type", "application/vnd.ms-excel; charset=utf-8");
|
||||||
res.setHeader("Content-Disposition", `attachment; filename="${fileName}"`);
|
res.setHeader("Content-Disposition", `attachment; filename="${fileName}"`);
|
||||||
|
|
||||||
/**
|
// En-tête SpreadsheetML 2003
|
||||||
* En-tête SpreadsheetML 2003
|
|
||||||
* Nécessaire pour que le fichier soit reconnu comme XLS par Excel
|
|
||||||
*/
|
|
||||||
res.write(
|
res.write(
|
||||||
`<?xml version="1.0"?>
|
`<?xml version="1.0"?>
|
||||||
<?mso-application progid="Excel.Sheet"?>
|
<?mso-application progid="Excel.Sheet"?>
|
||||||
|
|
@ -464,74 +500,40 @@ router.post("/export/xls", async (req, res) => {
|
||||||
`</Row>\n`
|
`</Row>\n`
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
// Traitement des parcours
|
||||||
* Écrit une page de résultats dans le fichier XLS
|
for (const parcours of allParcours) {
|
||||||
* Récupère les clients avec fallback si l'expand ne fonctionne pas
|
const contrat = parcours["@expand"]?.contrat || null;
|
||||||
*/
|
const intermediaire = contrat ? (contrat["@expand"]?.intermediaire || null) : null;
|
||||||
const writePage = async (result) => {
|
|
||||||
for (const parcours of result.items) {
|
// Client : d'abord depuis l'expand, sinon depuis le batch
|
||||||
const contrat = parcours["@expand"]?.contrat || null;
|
let client = contrat ? (contrat["@expand"]?.client || null) : null;
|
||||||
const intermediaire = contrat ? (contrat["@expand"]?.intermediaire || null) : null;
|
if (!client && contrat && contrat.client) {
|
||||||
|
client = clientsMap.get(contrat.client) || null;
|
||||||
/**
|
|
||||||
* Récupération du client avec fallback
|
|
||||||
* L'expand PocketBase ne fonctionne pas toujours, donc récupération directe si nécessaire
|
|
||||||
*/
|
|
||||||
let client = contrat ? (contrat["@expand"]?.client || null) : null;
|
|
||||||
if (!client && contrat && contrat.client) {
|
|
||||||
try {
|
|
||||||
client = await clientService.getClient(contrat.client);
|
|
||||||
} catch (err) {
|
|
||||||
// Erreur silencieuse : client non trouvé ou erreur de récupération
|
|
||||||
client = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const lastUser = parcours["@expand"]?.dernierUtilisateur;
|
|
||||||
const region = lastUser?.["@expand"]?.region;
|
|
||||||
|
|
||||||
const row = [
|
|
||||||
parcours.numParcours,
|
|
||||||
fmtDateFR(parcours.created),
|
|
||||||
lastUser?.matricule || "NC",
|
|
||||||
lastUser ? `${lastUser.prenom || ""} ${lastUser.nom || ""}`.trim() || "NC" : "NC",
|
|
||||||
region ? (region.nom || "NC") : "NC",
|
|
||||||
contrat ? (contrat.numSaisine || "NC") : "NC",
|
|
||||||
contrat ? (contrat.numContrat || "NC") : "NC",
|
|
||||||
contrat ? (contrat.produit || "NC") : "NC",
|
|
||||||
contrat ? (contrat.type || "NC") : "NC",
|
|
||||||
intermediaire ? (intermediaire.numPortefeuille || "NC") : "NC",
|
|
||||||
intermediaire ? (intermediaire.nom || "NC") : "NC",
|
|
||||||
client ? (client.numClient || "NC") : "NC",
|
|
||||||
client ? (client.nom || "NC") : "NC",
|
|
||||||
].map(v => String(v).replace(/\r?\n/g, " "));
|
|
||||||
|
|
||||||
res.write(rowXml(row) + "\n");
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
const lastUser = parcours["@expand"]?.dernierUtilisateur;
|
||||||
|
const region = lastUser?.["@expand"]?.region;
|
||||||
|
|
||||||
await writePage(first);
|
const row = [
|
||||||
|
parcours.numParcours,
|
||||||
|
fmtDateFR(parcours.created),
|
||||||
|
lastUser?.matricule || "NC",
|
||||||
|
lastUser ? `${lastUser.prenom || ""} ${lastUser.nom || ""}`.trim() || "NC" : "NC",
|
||||||
|
region ? (region.nom || "NC") : "NC",
|
||||||
|
contrat ? (contrat.numSaisine || "NC") : "NC",
|
||||||
|
contrat ? (contrat.numContrat || "NC") : "NC",
|
||||||
|
contrat ? (contrat.produit || "NC") : "NC",
|
||||||
|
contrat ? (contrat.type || "NC") : "NC",
|
||||||
|
intermediaire ? (intermediaire.numPortefeuille || "NC") : "NC",
|
||||||
|
intermediaire ? (intermediaire.nom || "NC") : "NC",
|
||||||
|
client ? (client.numClient || "NC") : "NC",
|
||||||
|
client ? (client.nom || "NC") : "NC",
|
||||||
|
].map(v => String(v).replace(/\r?\n/g, " "));
|
||||||
|
|
||||||
/**
|
res.write(rowXml(row) + "\n");
|
||||||
* Traitement des pages suivantes
|
|
||||||
* Export par pages de 1000 parcours pour éviter la surcharge mémoire
|
|
||||||
*/
|
|
||||||
const totalPages = first.totalPages || 1;
|
|
||||||
while (page < totalPages) {
|
|
||||||
page++;
|
|
||||||
try {
|
|
||||||
const next = await parcoursService.getParcoursByRegionsPage(
|
|
||||||
effective.regions, page, perPage, { filter, sort }
|
|
||||||
);
|
|
||||||
await writePage(next);
|
|
||||||
} catch (e) {
|
|
||||||
logger.log("error", e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Fermeture du fichier XML SpreadsheetML
|
||||||
* Fermeture du fichier XML SpreadsheetML
|
|
||||||
*/
|
|
||||||
res.write(
|
res.write(
|
||||||
` </Table>
|
` </Table>
|
||||||
</Worksheet>
|
</Worksheet>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,55 @@ async function getClient(id) {
|
||||||
return globalService.fetchInfoByCriteria("client", criteria);
|
return globalService.fetchInfoByCriteria("client", criteria);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère plusieurs clients en plusieurs requêtes batch (optimisation pour exports)
|
||||||
|
* Découpe en chunks de 50 IDs pour éviter les filtres trop longs
|
||||||
|
* SDK 0.7.x : getFullList(collection, batchSize, options)
|
||||||
|
* @param {string[]} clientIds - Tableau d'IDs de clients
|
||||||
|
* @returns {Map<string, Object>} - Map des clients par ID
|
||||||
|
*/
|
||||||
|
async function getClientsBatch(clientIds) {
|
||||||
|
const clientMap = new Map();
|
||||||
|
if (!clientIds || clientIds.length === 0) return clientMap;
|
||||||
|
|
||||||
|
// Filtrer les IDs valides et uniques
|
||||||
|
const uniqueIds = [...new Set(clientIds.filter(id => id && typeof id === 'string'))];
|
||||||
|
if (uniqueIds.length === 0) return clientMap;
|
||||||
|
|
||||||
|
// Découper en chunks de 50 pour éviter les filtres trop longs
|
||||||
|
const CHUNK_SIZE = 50;
|
||||||
|
const chunks = [];
|
||||||
|
for (let i = 0; i < uniqueIds.length; i += CHUNK_SIZE) {
|
||||||
|
chunks.push(uniqueIds.slice(i, i + CHUNK_SIZE));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traiter chaque chunk
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
try {
|
||||||
|
// Construire le filtre OR pour ce chunk
|
||||||
|
const filter = chunk.map(id => `id = "${id}"`).join(" || ");
|
||||||
|
|
||||||
|
// SDK 0.7.x : getFullList(collection, batchSize, options)
|
||||||
|
const clients = await db.records.getFullList("client", 500, {
|
||||||
|
filter: filter,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ajouter à la map
|
||||||
|
clients.forEach(client => {
|
||||||
|
if (client && client.id) {
|
||||||
|
clientMap.set(client.id, client);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.log("warn", `Erreur récupération clients chunk (${chunk.length} IDs):`, err?.message || String(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientMap;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createClient,
|
createClient,
|
||||||
getClient,
|
getClient,
|
||||||
|
getClientsBatch,
|
||||||
};
|
};
|
||||||
|
|
@ -21,16 +21,30 @@ async function getParcoursByNumParcours(numParcours) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Full list (batch côté PocketBase). | Fetch l'ensemble de la BD via chunk "batch"
|
* Full list (batch côté PocketBase). | Fetch l'ensemble de la BD via chunk "batch"
|
||||||
|
* SDK 0.7.x : getFullList(collection, batchSize, options)
|
||||||
*/
|
*/
|
||||||
async function getParcoursFullList({ filter, sort, expand, fields, batch = 500, skipTotal = true }) {
|
async function getParcoursFullList({ filter, sort, expand, fields, batch = 500 }) {
|
||||||
return db.records.getFullList("parcours", {
|
const options = {
|
||||||
filter: filter || "",
|
|
||||||
sort: sort || "-created",
|
sort: sort || "-created",
|
||||||
expand,
|
};
|
||||||
fields,
|
|
||||||
batch, // taille interne des sous-requêtes
|
// Ajouter expand si défini
|
||||||
skipTotal, // évite le COUNT total (évite calcul inutile + gain de temps)
|
if (expand) {
|
||||||
});
|
options.expand = expand;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter fields si défini
|
||||||
|
if (fields) {
|
||||||
|
options.fields = fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter filter SEULEMENT s'il n'est pas vide (SDK 0.7.x rejette les filtres vides)
|
||||||
|
if (filter && filter.trim() !== "") {
|
||||||
|
options.filter = filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SDK 0.7.x : getFullList(collection, batchSize, options)
|
||||||
|
return db.records.getFullList("parcours", batch, options);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Pagination multi-régions + filtres/tri optionnels (server-side DataTables)
|
* Pagination multi-régions + filtres/tri optionnels (server-side DataTables)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue