fix : exports (csv, xls) fixed, clients expand, fixed
This commit is contained in:
parent
4f82b29f18
commit
5de492b308
|
|
@ -275,52 +275,64 @@ router.post("/export/csv", async (req, res) => {
|
|||
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;
|
||||
|
||||
while (true) {
|
||||
if (aborted) break;
|
||||
// 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(" && ");
|
||||
|
||||
let result;
|
||||
// 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 {
|
||||
result = await parcoursService.getParcoursByRegionsPage(
|
||||
effective.regions,
|
||||
page,
|
||||
perPage,
|
||||
{ filter, sort }
|
||||
);
|
||||
allParcours = await parcoursService.getParcoursFullList({
|
||||
filter: finalFilter,
|
||||
sort: sort || "-created",
|
||||
expand: expandFields,
|
||||
batch: 500,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.log("error", err);
|
||||
break;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Traitement séquentiel de chaque parcours avec récupération des clients
|
||||
*/
|
||||
for (const parcours of result.items) {
|
||||
// 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;
|
||||
|
||||
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
|
||||
*/
|
||||
// Client : d'abord depuis l'expand, sinon depuis le batch
|
||||
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;
|
||||
}
|
||||
client = clientsMap.get(contrat.client) || null;
|
||||
}
|
||||
|
||||
const lastUser = parcours["@expand"]?.dernierUtilisateur;
|
||||
const region = lastUser?.["@expand"]?.region;
|
||||
|
||||
|
|
@ -340,10 +352,6 @@ router.post("/export/csv", async (req, res) => {
|
|||
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");
|
||||
|
|
@ -354,11 +362,6 @@ router.post("/export/csv", async (req, res) => {
|
|||
}
|
||||
}
|
||||
|
||||
// Passage à la page suivante si nécessaire
|
||||
if (aborted || page >= result.totalPages) break;
|
||||
page++;
|
||||
}
|
||||
|
||||
if (!aborted) {
|
||||
res.end();
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
const first = await parcoursService.getParcoursByRegionsPage(
|
||||
effective.regions, page, perPage, { filter, sort }
|
||||
);
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
const fileName = (mode === "full")
|
||||
? "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-Disposition", `attachment; filename="${fileName}"`);
|
||||
|
||||
/**
|
||||
* En-tête SpreadsheetML 2003
|
||||
* Nécessaire pour que le fichier soit reconnu comme XLS par Excel
|
||||
*/
|
||||
// En-tête SpreadsheetML 2003
|
||||
res.write(
|
||||
`<?xml version="1.0"?>
|
||||
<?mso-application progid="Excel.Sheet"?>
|
||||
|
|
@ -464,28 +500,17 @@ router.post("/export/xls", async (req, res) => {
|
|||
`</Row>\n`
|
||||
);
|
||||
|
||||
/**
|
||||
* Écrit une page de résultats dans le fichier XLS
|
||||
* Récupère les clients avec fallback si l'expand ne fonctionne pas
|
||||
*/
|
||||
const writePage = async (result) => {
|
||||
for (const parcours of result.items) {
|
||||
// Traitement des parcours
|
||||
for (const parcours of allParcours) {
|
||||
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
|
||||
*/
|
||||
// Client : d'abord depuis l'expand, sinon depuis le batch
|
||||
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;
|
||||
}
|
||||
client = clientsMap.get(contrat.client) || null;
|
||||
}
|
||||
|
||||
const lastUser = parcours["@expand"]?.dernierUtilisateur;
|
||||
const region = lastUser?.["@expand"]?.region;
|
||||
|
||||
|
|
@ -507,31 +532,8 @@ router.post("/export/xls", async (req, res) => {
|
|||
|
||||
res.write(rowXml(row) + "\n");
|
||||
}
|
||||
};
|
||||
|
||||
await writePage(first);
|
||||
|
||||
/**
|
||||
* 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(
|
||||
` </Table>
|
||||
</Worksheet>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,55 @@ async function getClient(id) {
|
|||
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 = {
|
||||
createClient,
|
||||
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"
|
||||
* SDK 0.7.x : getFullList(collection, batchSize, options)
|
||||
*/
|
||||
async function getParcoursFullList({ filter, sort, expand, fields, batch = 500, skipTotal = true }) {
|
||||
return db.records.getFullList("parcours", {
|
||||
filter: filter || "",
|
||||
async function getParcoursFullList({ filter, sort, expand, fields, batch = 500 }) {
|
||||
const options = {
|
||||
sort: sort || "-created",
|
||||
expand,
|
||||
fields,
|
||||
batch, // taille interne des sous-requêtes
|
||||
skipTotal, // évite le COUNT total (évite calcul inutile + gain de temps)
|
||||
});
|
||||
};
|
||||
|
||||
// Ajouter expand si défini
|
||||
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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue