diff --git a/client/src/pages/Admin.tsx b/client/src/pages/Admin.tsx index ec857ca..ac1c5db 100644 --- a/client/src/pages/Admin.tsx +++ b/client/src/pages/Admin.tsx @@ -23,6 +23,7 @@ import { X, } from "lucide-react"; import { useState, useMemo, Fragment, useRef, useCallback } from "react"; +import * as XLSX from "xlsx"; import { toast } from "sonner"; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -863,14 +864,25 @@ function ImportEtablissementsPanel() { setDone(0); }, []); - const handleFile = (e: React.ChangeEvent) => { + const handleFile = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; + const isXlsx = file.name.endsWith(".xlsx") || file.name.endsWith(".xls"); const reader = new FileReader(); - reader.onload = (ev) => parseCSV(ev.target?.result as string); - reader.readAsText(file, "UTF-8"); + if (isXlsx) { + reader.onload = (ev) => { + const data = new Uint8Array(ev.target?.result as ArrayBuffer); + const wb = XLSX.read(data, { type: "array" }); + const ws = wb.Sheets[wb.SheetNames[0]]; + const csv = XLSX.utils.sheet_to_csv(ws, { FS: ";" }); + parseCSV(csv); + }; + reader.readAsArrayBuffer(file); + } else { + reader.onload = (ev) => parseCSV(ev.target?.result as string); + reader.readAsText(file, "UTF-8"); + } }; - const handleImport = async () => { setImporting(true); let count = 0; @@ -939,7 +951,7 @@ function ImportEtablissementsPanel() {

Cliquez pour sélectionner un fichier CSV

Séparateur : point-virgule, virgule ou tabulation

- + {/* Colonnes attendues */} @@ -1078,11 +1090,22 @@ function ImportContactsPanel() { const handleFile = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; + const isXlsx = file.name.endsWith(".xlsx") || file.name.endsWith(".xls"); const reader = new FileReader(); - reader.onload = (ev) => parseCSV(ev.target?.result as string); - reader.readAsText(file, "UTF-8"); + if (isXlsx) { + reader.onload = (ev) => { + const data = new Uint8Array(ev.target?.result as ArrayBuffer); + const wb = XLSX.read(data, { type: "array" }); + const ws = wb.Sheets[wb.SheetNames[0]]; + const csv = XLSX.utils.sheet_to_csv(ws, { FS: ";" }); + parseCSV(csv); + }; + reader.readAsArrayBuffer(file); + } else { + reader.onload = (ev) => parseCSV(ev.target?.result as string); + reader.readAsText(file, "UTF-8"); + } }; - const handleImport = async () => { setImporting(true); let count = 0; @@ -1150,7 +1173,7 @@ function ImportContactsPanel() {

Cliquez pour sélectionner un fichier CSV

Séparateur : point-virgule, virgule ou tabulation

- + {/* Colonnes attendues */} diff --git a/package.json b/package.json index 0885715..0822326 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "tailwindcss-animate": "^1.0.7", "vaul": "^1.1.2", "wouter": "^3.3.5", + "xlsx": "^0.18.5", "zod": "^4.1.12" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84eff17..98fe72f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -208,6 +208,9 @@ importers: wouter: specifier: ^3.3.5 version: 3.7.1(patch_hash=4e16e6ff3fde7d6c1024d3e0c8605dc9eb6afb690d0d49958c2f449091813072)(react@19.2.1) + xlsx: + specifier: ^0.18.5 + version: 0.18.5 zod: specifier: ^4.1.12 version: 4.1.12 @@ -2389,6 +2392,10 @@ packages: add@2.0.6: resolution: {integrity: sha512-j5QzrmsokwWWp6kUcJQySpbG+xfOBqqKnup3OIk1pz+kB/80SLorZ9V8zHFLO92Lcd+hbvq8bT+zOGoPkmBV0Q==} + adler-32@1.3.1: + resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} + engines: {node: '>=0.8'} + aria-hidden@1.2.6: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} @@ -2465,6 +2472,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + cfb@1.2.2: + resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==} + engines: {node: '>=0.8'} + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -2510,6 +2521,10 @@ packages: react: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc + codepage@1.15.0: + resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} + engines: {node: '>=0.8'} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -2563,6 +2578,11 @@ packages: cose-base@2.2.0: resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -3059,6 +3079,10 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} + frac@1.1.2: + resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==} + engines: {node: '>=0.8'} + fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -3972,6 +3996,10 @@ packages: resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} engines: {node: '>= 0.6'} + ssf@0.11.2: + resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} + engines: {node: '>=0.8'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -4317,11 +4345,24 @@ packages: engines: {node: '>=8'} hasBin: true + wmf@1.0.2: + resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} + engines: {node: '>=0.8'} + + word@0.3.0: + resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} + engines: {node: '>=0.8'} + wouter@3.7.1: resolution: {integrity: sha512-od5LGmndSUzntZkE2R5CHhoiJ7YMuTIbiXsa0Anytc2RATekgv4sfWRAxLEULBrp7ADzinWQw8g470lkT8+fOw==} peerDependencies: react: '>=16.8.0' + xlsx@0.18.5: + resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} + engines: {node: '>=0.8'} + hasBin: true + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -6757,6 +6798,8 @@ snapshots: add@2.0.6: {} + adler-32@1.3.1: {} + aria-hidden@1.2.6: dependencies: tslib: 2.8.1 @@ -6840,6 +6883,11 @@ snapshots: ccount@2.0.1: {} + cfb@1.2.2: + dependencies: + adler-32: 1.3.1 + crc-32: 1.2.2 + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -6892,6 +6940,8 @@ snapshots: - '@types/react' - '@types/react-dom' + codepage@1.15.0: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -6932,6 +6982,8 @@ snapshots: dependencies: layout-base: 2.0.1 + crc-32@1.2.2: {} + cssesc@3.0.0: {} csstype@3.1.3: {} @@ -7419,6 +7471,8 @@ snapshots: forwarded@0.2.0: {} + frac@1.1.2: {} + fraction.js@4.3.7: {} framer-motion@12.23.22(react-dom@19.2.1(react@19.2.1))(react@19.2.1): @@ -8681,6 +8735,10 @@ snapshots: sqlstring@2.3.3: {} + ssf@0.11.2: + dependencies: + frac: 1.1.2 + stackback@0.0.2: {} statuses@2.0.1: {} @@ -9026,6 +9084,10 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wmf@1.0.2: {} + + word@0.3.0: {} + wouter@3.7.1(patch_hash=4e16e6ff3fde7d6c1024d3e0c8605dc9eb6afb690d0d49958c2f449091813072)(react@19.2.1): dependencies: mitt: 3.0.1 @@ -9033,6 +9095,16 @@ snapshots: regexparam: 3.0.0 use-sync-external-store: 1.6.0(react@19.2.1) + xlsx@0.18.5: + dependencies: + adler-32: 1.3.1 + cfb: 1.2.2 + codepage: 1.15.0 + crc-32: 1.2.2 + ssf: 0.11.2 + wmf: 1.0.2 + word: 0.3.0 + yallist@3.1.1: {} yallist@5.0.0: {} diff --git a/todo.md b/todo.md index 1555362..d2a9587 100644 --- a/todo.md +++ b/todo.md @@ -64,9 +64,9 @@ ## Évolution v4 -- [ ] CGU : afficher à chaque connexion (réinitialiser à chaque login, pas seulement à la fermeture du navigateur) -- [ ] Moteur de recherche : corriger les filtres multicritères qui ne fonctionnent pas -- [ ] Page "Mes Solutions Numériques" : liste de toutes mes solutions avec les établissements les utilisant (accordéon) -- [ ] Page "Solutions Logicielles" : liste globale des solutions avec établissements équipés (accordéon), accessible depuis la sidebar -- [ ] Admin : onglet import établissements (CSV/Excel) -- [ ] Admin : onglet import contacts (CSV/Excel) +- [x] CGU : afficher à chaque connexion (réinitialiser à chaque login, pas seulement à la fermeture du navigateur) +- [x] Moteur de recherche : corriger les filtres multicritères qui ne fonctionnent pas +- [x] Page "Mes Solutions Numériques" : liste de toutes mes solutions avec les établissements les utilisant (accordéon) +- [x] Page "Solutions Logicielles" : liste globale des solutions avec établissements équipés (accordéon), accessible depuis la sidebar +- [x] Admin : onglet import établissements (CSV/Excel) +- [x] Admin : onglet import contacts (CSV/Excel)