Checkpoint: feat: icône verte si solution utilisée par au moins 1 établissement - modification de SolutionsLogicielles.tsx pour afficher l'icône Package en vert (text-green-600, bg-green-500/10) si nbEtablissements > 0, en bleu sinon. Applicable aux deux vues : accordéon et liste à plat.

This commit is contained in:
Manus
2026-04-24 06:17:45 -04:00
parent 8c0c75fb17
commit b69f5ff118
2 changed files with 101 additions and 3 deletions

View File

@@ -195,8 +195,8 @@ export default function SolutionsLogicielles() {
className="flex-1 flex items-center justify-between px-5 py-4 text-left hover:bg-muted/30 transition-colors" className="flex-1 flex items-center justify-between px-5 py-4 text-left hover:bg-muted/30 transition-colors"
> >
<div className="flex items-center gap-4 min-w-0"> <div className="flex items-center gap-4 min-w-0">
<div className="w-9 h-9 rounded-lg bg-blue-500/10 flex items-center justify-center flex-shrink-0"> <div className={`w-9 h-9 rounded-lg flex items-center justify-center flex-shrink-0 ${sol.nbEtablissements > 0 ? 'bg-green-500/10' : 'bg-blue-500/10'}`}>
<Package size={16} className="text-blue-600" /> <Package size={16} className={sol.nbEtablissements > 0 ? 'text-green-600' : 'text-blue-600'} />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<div className="font-semibold text-foreground truncate">{sol.solutionNom}</div> <div className="font-semibold text-foreground truncate">{sol.solutionNom}</div>
@@ -312,7 +312,7 @@ export default function SolutionsLogicielles() {
<> <>
<td className={`px-4 py-3 font-semibold text-foreground align-top ${rowBgSpan}`} rowSpan={sol.etablissements.length}> <td className={`px-4 py-3 font-semibold text-foreground align-top ${rowBgSpan}`} rowSpan={sol.etablissements.length}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Package size={13} className="text-blue-500 flex-shrink-0" /> <Package size={13} className={sol.nbEtablissements > 0 ? 'text-green-600 flex-shrink-0' : 'text-blue-500 flex-shrink-0'} />
{sol.solutionNom} {sol.solutionNom}
</div> </div>
</td> </td>

98
export_sandbox_data.mjs Normal file
View File

@@ -0,0 +1,98 @@
/**
* Export des données de la base Sandbox SONUM vers un script SQL d'injection.
* Tables exportées : blocs_fonctionnels, editeurs, etablissements, solutions,
* logiciels_etablissements, consultations, demandes_contact, user_etablissements
* Tables exclues : users, local_credentials, __drizzle_migrations
*/
import mysql from 'mysql2/promise';
import fs from 'fs';
const TABLES_TO_EXPORT = [
'blocs_fonctionnels',
'editeurs',
'etablissements',
'solutions',
'logiciels_etablissements',
'consultations',
'demandes_contact',
'user_etablissements',
];
const conn = await mysql.createConnection(process.env.DATABASE_URL);
let sql = `-- Export données SONUM Sandbox → VPS
-- Généré le ${new Date().toISOString()}
-- Tables : ${TABLES_TO_EXPORT.join(', ')}
-- ATTENTION : exclut users, local_credentials, __drizzle_migrations
SET FOREIGN_KEY_CHECKS = 0;
SET SQL_MODE = 'NO_AUTO_VALUE_ON_ZERO';
`;
for (const table of TABLES_TO_EXPORT) {
console.log(`Exporting table: ${table}...`);
// Récupérer la structure de la table
const [[createResult]] = await conn.execute(`SHOW CREATE TABLE \`${table}\``);
const createSQL = createResult['Create Table'];
// Récupérer les données
const [rows] = await conn.execute(`SELECT * FROM \`${table}\``);
sql += `-- ─────────────────────────────────────────────────────────────────\n`;
sql += `-- Table: ${table} (${rows.length} lignes)\n`;
sql += `-- ─────────────────────────────────────────────────────────────────\n`;
sql += `TRUNCATE TABLE \`${table}\`;\n\n`;
if (rows.length === 0) {
sql += `-- (aucune donnée)\n\n`;
continue;
}
// Générer les INSERT par lots de 50
const columns = Object.keys(rows[0]);
const colList = columns.map(c => `\`${c}\``).join(', ');
const BATCH_SIZE = 50;
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
const batch = rows.slice(i, i + BATCH_SIZE);
const values = batch.map(row => {
const vals = columns.map(col => {
const v = row[col];
if (v === null || v === undefined) return 'NULL';
if (typeof v === 'number' || typeof v === 'bigint') return String(v);
if (typeof v === 'boolean') return v ? '1' : '0';
if (v instanceof Date) return `'${v.toISOString().slice(0, 19).replace('T', ' ')}'`;
// Escape string
const escaped = String(v)
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\0/g, '\\0');
return `'${escaped}'`;
});
return `(${vals.join(', ')})`;
});
sql += `INSERT INTO \`${table}\` (${colList}) VALUES\n${values.join(',\n')};\n`;
}
sql += `\n`;
}
sql += `SET FOREIGN_KEY_CHECKS = 1;\n`;
sql += `-- Fin de l'export\n`;
await conn.end();
const outputPath = '/home/ubuntu/sonum_data_export.sql';
fs.writeFileSync(outputPath, sql, 'utf8');
const stats = fs.statSync(outputPath);
console.log(`\nExport terminé : ${outputPath}`);
console.log(`Taille : ${(stats.size / 1024).toFixed(1)} KB`);
console.log(`\nRésumé par table :`);
for (const table of TABLES_TO_EXPORT) {
const matches = sql.match(new RegExp(`-- Table: ${table} \\((\\d+) lignes\\)`));
if (matches) console.log(` ${table}: ${matches[1]} lignes`);
}