From 2a2738f5c0249aa6a8b1e67289bd57cbbf2ddf7d Mon Sep 17 00:00:00 2001
From: Yannick Le Duc
Date: Tue, 16 Sep 2025 15:45:28 +0200
Subject: [PATCH] =?UTF-8?q?-=20am=C3=A9liore=20l'export/import=20(format?=
=?UTF-8?q?=20de=20fichiers=20en=20param=C3=A8tres,=20am=C3=A9lioration=20?=
=?UTF-8?q?de=20la=20robustesse,=20)=20-=20ajout=20bouton=20tout=20effacer?=
=?UTF-8?q?=20des=20propositions=20et=20participants?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
package-lock.json | 188 ++++++++++-
package.json | 2 +
.../campaigns/[id]/participants/page.tsx | 36 ++-
.../campaigns/[id]/propositions/page.tsx | 45 ++-
src/app/admin/page.tsx | 24 +-
src/app/admin/settings/page.tsx | 206 ++++++++++--
src/components/ClearAllParticipantsModal.tsx | 125 ++++++++
src/components/ClearAllPropositionsModal.tsx | 124 ++++++++
src/components/ExportFileFormatSelect.tsx | 46 +++
src/components/ExportPropositionsButton.tsx | 59 ++++
src/components/ExportStatsButton.tsx | 10 +-
src/components/ImportFileModal.tsx | 10 +-
src/components/ShareModal.tsx | 221 +++++++++++++
src/lib/export-utils.ts | 301 +++++++++++++++++-
src/lib/file-utils.ts | 121 +++++--
src/lib/services.ts | 18 ++
16 files changed, 1455 insertions(+), 81 deletions(-)
create mode 100644 src/components/ClearAllParticipantsModal.tsx
create mode 100644 src/components/ClearAllPropositionsModal.tsx
create mode 100644 src/components/ExportFileFormatSelect.tsx
create mode 100644 src/components/ExportPropositionsButton.tsx
create mode 100644 src/components/ShareModal.tsx
diff --git a/package-lock.json b/package-lock.json
index 9502558..afe58b8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,6 +21,7 @@
"@supabase/supabase-js": "^2.56.0",
"@types/dompurify": "^3.0.5",
"@types/nodemailer": "^7.0.1",
+ "@types/qrcode": "^1.5.5",
"@types/xlsx": "^0.0.35",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -29,6 +30,7 @@
"lucide-react": "^0.541.0",
"next": "15.5.0",
"nodemailer": "^7.0.5",
+ "qrcode": "^1.5.4",
"react": "19.1.0",
"react-dom": "19.1.0",
"tailwind-merge": "^3.3.1",
@@ -5105,6 +5107,15 @@
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
"license": "MIT"
},
+ "node_modules/@types/qrcode": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz",
+ "integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/react": {
"version": "19.1.11",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.11.tgz",
@@ -5861,7 +5872,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -5871,7 +5881,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
@@ -6423,7 +6432,6 @@
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -6637,7 +6645,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
@@ -6650,7 +6657,6 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/color-string": {
@@ -6922,6 +6928,15 @@
}
}
},
+ "node_modules/decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/decimal.js": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
@@ -7054,6 +7069,12 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
+ "node_modules/dijkstrajs": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
+ "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
+ "license": "MIT"
+ },
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@@ -8189,7 +8210,6 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
- "dev": true,
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
@@ -8855,7 +8875,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -11392,7 +11411,6 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -11447,7 +11465,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -11632,6 +11649,15 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/pngjs": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
+ "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -11785,6 +11811,127 @@
],
"license": "MIT"
},
+ "node_modules/qrcode": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
+ "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
+ "license": "MIT",
+ "dependencies": {
+ "dijkstrajs": "^1.0.1",
+ "pngjs": "^5.0.0",
+ "yargs": "^15.3.1"
+ },
+ "bin": {
+ "qrcode": "bin/qrcode"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/qrcode/node_modules/cliui": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
+ }
+ },
+ "node_modules/qrcode/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/qrcode/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/qrcode/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/qrcode/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/qrcode/node_modules/y18n": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
+ "license": "ISC"
+ },
+ "node_modules/qrcode/node_modules/yargs": {
+ "version": "15.4.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/qrcode/node_modules/yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+ "license": "ISC",
+ "dependencies": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
@@ -11972,12 +12119,17 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
+ "node_modules/require-main-filename": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+ "license": "ISC"
+ },
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@@ -12188,6 +12340,12 @@
"node": ">=10"
}
},
+ "node_modules/set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+ "license": "ISC"
+ },
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -12541,7 +12699,6 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
@@ -12556,7 +12713,6 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true,
"license": "MIT"
},
"node_modules/string.prototype.includes": {
@@ -12676,7 +12832,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
@@ -13476,6 +13631,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/which-module": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
+ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
+ "license": "ISC"
+ },
"node_modules/which-typed-array": {
"version": "1.1.19",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
@@ -13530,7 +13691,6 @@
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
diff --git a/package.json b/package.json
index 651e6be..c1fa8d1 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
"@supabase/supabase-js": "^2.56.0",
"@types/dompurify": "^3.0.5",
"@types/nodemailer": "^7.0.1",
+ "@types/qrcode": "^1.5.5",
"@types/xlsx": "^0.0.35",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -37,6 +38,7 @@
"lucide-react": "^0.541.0",
"next": "15.5.0",
"nodemailer": "^7.0.5",
+ "qrcode": "^1.5.4",
"react": "19.1.0",
"react-dom": "19.1.0",
"tailwind-merge": "^3.3.1",
diff --git a/src/app/admin/campaigns/[id]/participants/page.tsx b/src/app/admin/campaigns/[id]/participants/page.tsx
index e342e35..ee5f9da 100644
--- a/src/app/admin/campaigns/[id]/participants/page.tsx
+++ b/src/app/admin/campaigns/[id]/participants/page.tsx
@@ -9,6 +9,7 @@ import EditParticipantModal from '@/components/EditParticipantModal';
import DeleteParticipantModal from '@/components/DeleteParticipantModal';
import ImportFileModal from '@/components/ImportFileModal';
import SendParticipantEmailModal from '@/components/SendParticipantEmailModal';
+import ClearAllParticipantsModal from '@/components/ClearAllParticipantsModal';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
@@ -31,6 +32,7 @@ function CampaignParticipantsPageContent() {
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showImportModal, setShowImportModal] = useState(false);
const [showSendEmailModal, setShowSendEmailModal] = useState(false);
+ const [showClearAllModal, setShowClearAllModal] = useState(false);
const [selectedParticipant, setSelectedParticipant] = useState(null);
const [copiedParticipantId, setCopiedParticipantId] = useState(null);
@@ -87,9 +89,9 @@ function CampaignParticipantsPageContent() {
try {
const participantsToCreate = data.map(row => ({
campaign_id: campaignId,
- first_name: row.first_name || '',
- last_name: row.last_name || '',
- email: row.email || ''
+ first_name: row.Prénom || '',
+ last_name: row.Nom || '',
+ email: row.Email || ''
}));
// Créer les participants un par un
@@ -103,6 +105,16 @@ function CampaignParticipantsPageContent() {
}
};
+ const handleClearAllParticipants = async () => {
+ try {
+ await participantService.deleteAllByCampaign(campaignId);
+ loadData();
+ } catch (error) {
+ console.error('Erreur lors de la suppression des participants:', error);
+ throw error;
+ }
+ };
+
const getInitials = (firstName: string, lastName: string) => {
return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
};
@@ -182,6 +194,16 @@ function CampaignParticipantsPageContent() {
Importer
+ {participants.length > 0 && (
+ setShowClearAllModal(true)}
+ className="text-red-600 border-red-200 hover:bg-red-50 hover:border-red-300 dark:text-red-400 dark:border-red-800 dark:hover:bg-red-900/20"
+ >
+
+ Tout effacer
+
+ )}
setShowAddModal(true)} size="lg">
✨ Nouveau participant
@@ -377,6 +399,14 @@ function CampaignParticipantsPageContent() {
campaign={campaign}
/>
)}
+
+ setShowClearAllModal(false)}
+ onConfirm={handleClearAllParticipants}
+ campaignTitle={campaign?.title}
+ participantCount={participants.length}
+ />
);
diff --git a/src/app/admin/campaigns/[id]/propositions/page.tsx b/src/app/admin/campaigns/[id]/propositions/page.tsx
index cd5c089..f888b8e 100644
--- a/src/app/admin/campaigns/[id]/propositions/page.tsx
+++ b/src/app/admin/campaigns/[id]/propositions/page.tsx
@@ -8,6 +8,8 @@ import AddPropositionModal from '@/components/AddPropositionModal';
import EditPropositionModal from '@/components/EditPropositionModal';
import DeletePropositionModal from '@/components/DeletePropositionModal';
import ImportFileModal from '@/components/ImportFileModal';
+import ExportPropositionsButton from '@/components/ExportPropositionsButton';
+import ClearAllPropositionsModal from '@/components/ClearAllPropositionsModal';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
@@ -29,6 +31,7 @@ function CampaignPropositionsPageContent() {
const [showEditModal, setShowEditModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showImportModal, setShowImportModal] = useState(false);
+ const [showClearAllModal, setShowClearAllModal] = useState(false);
const [selectedProposition, setSelectedProposition] = useState(null);
useEffect(() => {
@@ -84,11 +87,11 @@ function CampaignPropositionsPageContent() {
try {
const propositionsToCreate = data.map(row => ({
campaign_id: campaignId,
- title: row.title || '',
- description: row.description || '',
- author_first_name: row.author_first_name || 'admin',
- author_last_name: row.author_last_name || 'admin',
- author_email: row.author_email || 'admin@example.com'
+ title: row.Titre || '',
+ description: row.Description || '',
+ author_first_name: row.Prénom || 'admin',
+ author_last_name: row.Nom || 'admin',
+ author_email: row.Email || 'admin@example.com'
}));
// Créer les propositions une par une
@@ -102,7 +105,15 @@ function CampaignPropositionsPageContent() {
}
};
-
+ const handleClearAllPropositions = async () => {
+ try {
+ await propositionService.deleteAllByCampaign(campaignId);
+ loadData();
+ } catch (error) {
+ console.error('Erreur lors de la suppression des propositions:', error);
+ throw error;
+ }
+ };
const getInitials = (firstName: string, lastName: string) => {
return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
@@ -171,6 +182,20 @@ function CampaignPropositionsPageContent() {
Importer
+
+ {propositions.length > 0 && (
+ setShowClearAllModal(true)}
+ className="text-red-600 border-red-200 hover:bg-red-50 hover:border-red-300 dark:text-red-400 dark:border-red-800 dark:hover:bg-red-900/20"
+ >
+
+ Tout effacer
+
+ )}
setShowAddModal(true)} size="lg">
✨ Nouvelle proposition
@@ -299,6 +324,14 @@ function CampaignPropositionsPageContent() {
type="propositions"
campaignTitle={campaign?.title}
/>
+
+ setShowClearAllModal(false)}
+ onConfirm={handleClearAllPropositions}
+ campaignTitle={campaign?.title}
+ propositionCount={propositions.length}
+ />
);
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
index 87ee60c..29ccce7 100644
--- a/src/app/admin/page.tsx
+++ b/src/app/admin/page.tsx
@@ -7,6 +7,7 @@ import { authService } from '@/lib/auth';
import CreateCampaignModal from '@/components/CreateCampaignModal';
import EditCampaignModal from '@/components/EditCampaignModal';
import DeleteCampaignModal from '@/components/DeleteCampaignModal';
+import ShareModal from '@/components/ShareModal';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
@@ -14,7 +15,7 @@ import { Badge } from '@/components/ui/badge';
import AuthGuard from '@/components/AuthGuard';
import Footer from '@/components/Footer';
-import { FolderOpen, Users, FileText, Plus, BarChart3, Settings, Check, Copy, Mail } from 'lucide-react';
+import { FolderOpen, Users, FileText, Plus, BarChart3, Settings, Check, Copy, Mail, Share2 } from 'lucide-react';
import StatusSwitch from '@/components/StatusSwitch';
import { MarkdownContent } from '@/components/MarkdownContent';
@@ -27,6 +28,7 @@ function AdminPageContent() {
const [showCreateModal, setShowCreateModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
+ const [showShareModal, setShowShareModal] = useState(false);
const [selectedCampaign, setSelectedCampaign] = useState(null);
const [copiedCampaignId, setCopiedCampaignId] = useState(null);
@@ -433,6 +435,18 @@ function AdminPageContent() {
)}
+ {
+ setSelectedCampaign(campaign);
+ setShowShareModal(true);
+ }}
+ title="Partager le lien"
+ >
+
+
@@ -474,6 +488,14 @@ function AdminPageContent() {
{selectedCampaign && (
setShowDeleteModal(false)} onSuccess={handleCampaignDeleted} campaign={selectedCampaign} />
)}
+ {selectedCampaign && (
+ setShowShareModal(false)}
+ campaignTitle={selectedCampaign.title}
+ depositUrl={`${window.location.origin}/p/${selectedCampaign.slug || 'campagne'}`}
+ />
+ )}
{/* Footer */}
diff --git a/src/app/admin/settings/page.tsx b/src/app/admin/settings/page.tsx
index f7c15ec..e303f10 100644
--- a/src/app/admin/settings/page.tsx
+++ b/src/app/admin/settings/page.tsx
@@ -12,6 +12,7 @@ import Footer from '@/components/Footer';
import SmtpSettingsForm from '@/components/SmtpSettingsForm';
import { Settings, Monitor, Save, CheckCircle, Mail, FileText, Download } from 'lucide-react';
import { ExportAnonymizationSelect, AnonymizationLevel } from '@/components/ExportAnonymizationSelect';
+import { ExportFileFormatSelect, ExportFileFormat } from '@/components/ExportFileFormatSelect';
export const dynamic = 'force-dynamic';
@@ -24,6 +25,18 @@ function SettingsPageContent() {
const [proposePageMessage, setProposePageMessage] = useState('');
const [footerMessage, setFooterMessage] = useState('');
const [exportAnonymization, setExportAnonymization] = useState('full');
+ const [exportFileFormat, setExportFileFormat] = useState('ods');
+
+ // États pour la détection des modifications
+ const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
+ const [originalValues, setOriginalValues] = useState<{
+ randomizePropositions: boolean;
+ proposePageMessage: string;
+ footerMessage: string;
+ exportAnonymization: AnonymizationLevel;
+ exportFileFormat: ExportFileFormat;
+ } | null>(null);
+ const [autoSaved, setAutoSaved] = useState(false);
useEffect(() => {
// Vérifier la configuration Supabase
@@ -42,6 +55,34 @@ function SettingsPageContent() {
loadSettings();
}, []);
+ // Détecter les modifications
+ useEffect(() => {
+ if (!originalValues) return;
+
+ const hasChanges =
+ randomizePropositions !== originalValues.randomizePropositions ||
+ proposePageMessage !== originalValues.proposePageMessage ||
+ footerMessage !== originalValues.footerMessage ||
+ exportAnonymization !== originalValues.exportAnonymization ||
+ exportFileFormat !== originalValues.exportFileFormat;
+
+ setHasUnsavedChanges(hasChanges);
+ }, [randomizePropositions, proposePageMessage, footerMessage, exportAnonymization, exportFileFormat, originalValues]);
+
+ // Avertissement avant de quitter la page
+ useEffect(() => {
+ const handleBeforeUnload = (e: BeforeUnloadEvent) => {
+ if (hasUnsavedChanges) {
+ e.preventDefault();
+ e.returnValue = 'Vous avez des modifications non sauvegardées. Êtes-vous sûr de vouloir quitter ?';
+ return e.returnValue;
+ }
+ };
+
+ window.addEventListener('beforeunload', handleBeforeUnload);
+ return () => window.removeEventListener('beforeunload', handleBeforeUnload);
+ }, [hasUnsavedChanges]);
+
const loadSettings = async () => {
try {
setLoading(true);
@@ -63,6 +104,19 @@ function SettingsPageContent() {
// Charger le niveau d'anonymisation des exports
const anonymizationValue = await settingsService.getStringValue('export_anonymization', 'full') as AnonymizationLevel;
setExportAnonymization(anonymizationValue);
+
+ // Charger le format de fichier d'export
+ const fileFormatValue = await settingsService.getStringValue('export_file_format', 'ods') as ExportFileFormat;
+ setExportFileFormat(fileFormatValue);
+
+ // Stocker les valeurs originales pour la détection des modifications
+ setOriginalValues({
+ randomizePropositions: randomizeValue,
+ proposePageMessage: messageValue,
+ footerMessage: footerValue,
+ exportAnonymization: anonymizationValue,
+ exportFileFormat: fileFormatValue
+ });
} catch (error) {
console.error('Erreur lors du chargement des paramètres:', error);
} finally {
@@ -72,17 +126,82 @@ function SettingsPageContent() {
const handleRandomizeChange = async (checked: boolean) => {
setRandomizePropositions(checked);
+ // Sauvegarde automatique pour ce paramètre
+ try {
+ await settingsService.setBooleanValue('randomize_propositions', checked);
+ // Mettre à jour les valeurs originales
+ if (originalValues) {
+ setOriginalValues({
+ ...originalValues,
+ randomizePropositions: checked
+ });
+ }
+ // Afficher la confirmation de sauvegarde automatique
+ setAutoSaved(true);
+ setTimeout(() => setAutoSaved(false), 2000);
+ } catch (error) {
+ console.error('Erreur lors de la sauvegarde automatique:', error);
+ }
+ };
+
+ const handleExportAnonymizationChange = async (value: AnonymizationLevel) => {
+ setExportAnonymization(value);
+ // Sauvegarde automatique pour ce paramètre
+ try {
+ await settingsService.setStringValue('export_anonymization', value);
+ // Mettre à jour les valeurs originales
+ if (originalValues) {
+ setOriginalValues({
+ ...originalValues,
+ exportAnonymization: value
+ });
+ }
+ // Afficher la confirmation de sauvegarde automatique
+ setAutoSaved(true);
+ setTimeout(() => setAutoSaved(false), 2000);
+ } catch (error) {
+ console.error('Erreur lors de la sauvegarde automatique:', error);
+ }
+ };
+
+ const handleExportFileFormatChange = async (value: ExportFileFormat) => {
+ setExportFileFormat(value);
+ // Sauvegarde automatique pour ce paramètre
+ try {
+ await settingsService.setStringValue('export_file_format', value);
+ // Mettre à jour les valeurs originales
+ if (originalValues) {
+ setOriginalValues({
+ ...originalValues,
+ exportFileFormat: value
+ });
+ }
+ // Afficher la confirmation de sauvegarde automatique
+ setAutoSaved(true);
+ setTimeout(() => setAutoSaved(false), 2000);
+ } catch (error) {
+ console.error('Erreur lors de la sauvegarde automatique:', error);
+ }
};
const handleSave = async () => {
try {
setSaving(true);
- await settingsService.setBooleanValue('randomize_propositions', randomizePropositions);
+ // Sauvegarder seulement les paramètres qui ne sont pas sauvegardés automatiquement
await settingsService.setStringValue('propose_page_message', proposePageMessage);
await settingsService.setStringValue('footer_message', footerMessage);
- await settingsService.setStringValue('export_anonymization', exportAnonymization);
+
+ // Mettre à jour les valeurs originales
+ setOriginalValues({
+ randomizePropositions,
+ proposePageMessage,
+ footerMessage,
+ exportAnonymization,
+ exportFileFormat
+ });
+
setSaved(true);
- setTimeout(() => setSaved(false), 2000);
+ setTimeout(() => setSaved(false), 3000); // Message plus long pour les textes
} catch (error) {
console.error('Erreur lors de la sauvegarde des paramètres:', error);
} finally {
@@ -115,13 +234,36 @@ function SettingsPageContent() {
-
Paramètres
-
Configurez les paramètres de l'application
+
+
Paramètres
+ {hasUnsavedChanges && (
+
+
+ Modifications non sauvegardées
+
+ )}
+ {autoSaved && (
+
+
+ Sauvegardé automatiquement
+
+ )}
+
+
+ {hasUnsavedChanges
+ ? 'Vous avez des modifications non sauvegardées. N\'oubliez pas de cliquer sur "Sauvegarder".'
+ : 'Configurez les paramètres de l\'application'
+ }
+
{saving ? (
<>
@@ -207,13 +349,25 @@ function SettingsPageContent() {
Ce texte apparaît sous le titre de la campagne pour inviter les utilisateurs à déposer des propositions.
-
{/* Footer Message Setting */}
@@ -226,13 +380,25 @@ function SettingsPageContent() {
Ce texte apparaît en bas des pages publiques. Vous pouvez utiliser [texte du lien](GITURL) pour insérer un lien vers le repository Git.
-
+