mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-11 17:29:37 +02:00
Feature: Support multi-images, Google Calendar et correction RTL
Images multiples : - Ajout table note_images dans la base de données - Routes POST/GET/DELETE /api/notes/:id/images - Dossier uploads/images séparé pour les images - Support de plusieurs images par note Google Calendar : - Ajout table calendar_events - Routes /api/calendar pour sync et récupération des événements - Box Rendez-vous dans l'interface avec affichage des événements à venir - Configuration Google Calendar dans l'admin (API key et Calendar ID) - Synchronisation manuelle avec Google Calendar API - Installation package googleapis Corrections : - Fix RichTextEditor: ajout dir="ltr" pour empêcher l'écriture RTL - Fix Image type conflict (lucide-react vs Note interface) - Ajout original_name à l'interface Image pour compatibilité Interface : - Box Rendez-vous empilée avec Todos et RSS dans la colonne droite - Nouvel onglet Google Calendar dans l'administration - Bouton "Synchroniser" pour rafraîchir les événements
This commit is contained in:
10 files changed
+946
-5
No files matched your search
@@ -74,6 +74,18 @@ function initDatabase() {
|
||||
)
|
||||
`);
|
||||
|
||||
// Table note_images (images attachées aux notes)
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS note_images (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
note_id INTEGER NOT NULL,
|
||||
filename TEXT NOT NULL,
|
||||
original_name TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
// Table note_files (fichiers attachés aux notes)
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS note_files (
|
||||
@@ -137,14 +149,35 @@ function initDatabase() {
|
||||
)
|
||||
`);
|
||||
|
||||
// Table calendar_events (événements Google Calendar)
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS calendar_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
google_event_id TEXT UNIQUE NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
start_time DATETIME NOT NULL,
|
||||
end_time DATETIME NOT NULL,
|
||||
location TEXT,
|
||||
color_id TEXT,
|
||||
html_link TEXT,
|
||||
synced_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
// Créer les index pour la performance
|
||||
db.run('CREATE INDEX IF NOT EXISTS idx_notes_user ON notes(user_id)');
|
||||
db.run('CREATE INDEX IF NOT EXISTS idx_note_todos ON note_todos(note_id)');
|
||||
db.run('CREATE INDEX IF NOT EXISTS idx_global_todos_user ON global_todos(user_id)');
|
||||
db.run('CREATE INDEX IF NOT EXISTS idx_note_images ON note_images(note_id)');
|
||||
db.run('CREATE INDEX IF NOT EXISTS idx_note_files ON note_files(note_id)');
|
||||
db.run('CREATE INDEX IF NOT EXISTS idx_settings_key ON settings(key)');
|
||||
db.run('CREATE INDEX IF NOT EXISTS idx_rss_articles_feed ON rss_articles(feed_id)');
|
||||
db.run('CREATE INDEX IF NOT EXISTS idx_rss_articles_date ON rss_articles(pub_date)');
|
||||
db.run('CREATE INDEX IF NOT EXISTS idx_calendar_events_user ON calendar_events(user_id)');
|
||||
db.run('CREATE INDEX IF NOT EXISTS idx_calendar_events_start ON calendar_events(start_time)');
|
||||
|
||||
logger.info('✓ Tables de base de données créées avec succès');
|
||||
|
||||
|
||||
Generated
+352
@@ -49,6 +49,7 @@
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"express-validator": "^7.0.1",
|
||||
"googleapis": "^166.0.0",
|
||||
"helmet": "^7.1.0",
|
||||
"input-otp": "^1.4.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
@@ -4338,6 +4339,15 @@
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bignumber.js": {
|
||||
"version": "9.3.1",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
|
||||
"integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
@@ -5121,6 +5131,15 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
|
||||
@@ -5822,6 +5841,12 @@
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@@ -5895,6 +5920,29 @@
|
||||
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fetch-blob": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-domexception": "^1.0.0",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20 || >= 14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
@@ -6067,6 +6115,18 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fetch-blob": "^3.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@@ -6167,6 +6227,143 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz",
|
||||
"integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"extend": "^3.0.2",
|
||||
"https-proxy-agent": "^7.0.1",
|
||||
"node-fetch": "^3.3.2",
|
||||
"rimraf": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios/node_modules/agent-base": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios/node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios/node_modules/glob": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^3.1.2",
|
||||
"minimatch": "^9.0.4",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^1.11.1"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios/node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios/node_modules/minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios/node_modules/node-fetch": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"fetch-blob": "^3.1.4",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios/node_modules/rimraf": {
|
||||
"version": "5.0.10",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
|
||||
"integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"glob": "^10.3.7"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "dist/esm/bin.mjs"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/gcp-metadata": {
|
||||
"version": "8.1.2",
|
||||
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
|
||||
"integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"gaxios": "^7.0.0",
|
||||
"google-logging-utils": "^1.0.0",
|
||||
"json-bigint": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
@@ -6275,6 +6472,83 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/google-auth-library": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz",
|
||||
"integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.0",
|
||||
"ecdsa-sig-formatter": "^1.0.11",
|
||||
"gaxios": "^7.0.0",
|
||||
"gcp-metadata": "^8.0.0",
|
||||
"google-logging-utils": "^1.0.0",
|
||||
"gtoken": "^8.0.0",
|
||||
"jws": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/google-auth-library/node_modules/jwa": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "^1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/google-auth-library/node_modules/jws": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
||||
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^2.0.0",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/google-logging-utils": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.2.tgz",
|
||||
"integrity": "sha512-YsFPGVgDFf4IzSwbwIR0iaFJQFmR5Jp7V1WuYSjuRgAm9yWqsMhKE9YPlL+wvFLnc/wMiFV4SQUD9Y/JMpxIxQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/googleapis": {
|
||||
"version": "166.0.0",
|
||||
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-166.0.0.tgz",
|
||||
"integrity": "sha512-Xz7yG+ihVvYMwSULKIMeTp6vDX95rrSbsaR3xUmgYjwQdAkchMQ5ndFeG5QZwcBOIyvJFvG7ZLIZE7mKW2X/hw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"google-auth-library": "^10.2.0",
|
||||
"googleapis-common": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/googleapis-common": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-8.0.0.tgz",
|
||||
"integrity": "sha512-66if47It7y+Sab3HMkwEXx1kCq9qUC9px8ZXoj1CMrmLmUw81GpbnsNlXnlyZyGbGPGcj+tDD9XsZ23m7GLaJQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"extend": "^3.0.2",
|
||||
"gaxios": "^7.0.0-rc.4",
|
||||
"google-auth-library": "^10.1.0",
|
||||
"qs": "^6.7.0",
|
||||
"url-template": "^2.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@@ -6301,6 +6575,40 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/gtoken": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz",
|
||||
"integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"gaxios": "^7.0.0",
|
||||
"jws": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/gtoken/node_modules/jwa": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "^1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/gtoken/node_modules/jws": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
||||
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^2.0.0",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
@@ -6733,6 +7041,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/json-bigint": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
|
||||
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bignumber.js": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/json-buffer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||
@@ -7426,6 +7743,26 @@
|
||||
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"deprecated": "Use your platform's native DOMException instead",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
@@ -9833,6 +10170,12 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/url-template": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
|
||||
"integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==",
|
||||
"license": "BSD"
|
||||
},
|
||||
"node_modules/use-callback-ref": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
|
||||
@@ -10056,6 +10399,15 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"express-validator": "^7.0.1",
|
||||
"googleapis": "^166.0.0",
|
||||
"helmet": "^7.1.0",
|
||||
"input-otp": "^1.4.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
// Routes pour Google Calendar
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { google } = require('googleapis');
|
||||
const { getAll, getOne, runQuery } = require('../config/database');
|
||||
const { authenticateToken } = require('../middleware/auth');
|
||||
const logger = require('../config/logger');
|
||||
|
||||
// Toutes les routes nécessitent authentification
|
||||
router.use(authenticateToken);
|
||||
|
||||
/**
|
||||
* GET /api/calendar/events
|
||||
* Récupérer les événements du calendrier
|
||||
*/
|
||||
router.get('/events', async (req, res) => {
|
||||
try {
|
||||
const limit = parseInt(req.query.limit) || 10;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// Récupérer les événements à venir depuis la base de données
|
||||
const events = await getAll(`
|
||||
SELECT
|
||||
id, google_event_id, title, description,
|
||||
start_time, end_time, location, html_link
|
||||
FROM calendar_events
|
||||
WHERE user_id = ? AND start_time >= ?
|
||||
ORDER BY start_time ASC
|
||||
LIMIT ?
|
||||
`, [req.user.id, now, limit]);
|
||||
|
||||
res.json(events || []);
|
||||
} catch (error) {
|
||||
logger.error('Erreur lors de la récupération des événements:', error);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/calendar/sync
|
||||
* Synchroniser avec Google Calendar
|
||||
*/
|
||||
router.post('/sync', async (req, res) => {
|
||||
try {
|
||||
// Récupérer les paramètres Google Calendar depuis settings
|
||||
const apiKey = await getOne("SELECT value FROM settings WHERE key = 'google_calendar_api_key'");
|
||||
const calendarId = await getOne("SELECT value FROM settings WHERE key = 'google_calendar_id'");
|
||||
|
||||
if (!apiKey || !apiKey.value) {
|
||||
return res.status(400).json({ error: 'Clé API Google Calendar non configurée' });
|
||||
}
|
||||
|
||||
if (!calendarId || !calendarId.value) {
|
||||
return res.status(400).json({ error: 'ID du calendrier non configuré' });
|
||||
}
|
||||
|
||||
// Créer un client Google Calendar
|
||||
const calendar = google.calendar({ version: 'v3', auth: apiKey.value });
|
||||
|
||||
// Récupérer les événements des 30 prochains jours
|
||||
const now = new Date();
|
||||
const futureDate = new Date();
|
||||
futureDate.setDate(futureDate.getDate() + 30);
|
||||
|
||||
const response = await calendar.events.list({
|
||||
calendarId: calendarId.value,
|
||||
timeMin: now.toISOString(),
|
||||
timeMax: futureDate.toISOString(),
|
||||
maxResults: 50,
|
||||
singleEvents: true,
|
||||
orderBy: 'startTime',
|
||||
});
|
||||
|
||||
const events = response.data.items || [];
|
||||
let syncedCount = 0;
|
||||
|
||||
// Sauvegarder chaque événement dans la base de données
|
||||
for (const event of events) {
|
||||
const startTime = event.start.dateTime || event.start.date;
|
||||
const endTime = event.end.dateTime || event.end.date;
|
||||
|
||||
// Vérifier si l'événement existe déjà
|
||||
const existing = await getOne(
|
||||
'SELECT id FROM calendar_events WHERE google_event_id = ? AND user_id = ?',
|
||||
[event.id, req.user.id]
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
// Mettre à jour l'événement existant
|
||||
await runQuery(`
|
||||
UPDATE calendar_events
|
||||
SET title = ?, description = ?, start_time = ?, end_time = ?,
|
||||
location = ?, html_link = ?, synced_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`, [
|
||||
event.summary || 'Sans titre',
|
||||
event.description || '',
|
||||
startTime,
|
||||
endTime,
|
||||
event.location || '',
|
||||
event.htmlLink || '',
|
||||
existing.id
|
||||
]);
|
||||
} else {
|
||||
// Créer un nouvel événement
|
||||
await runQuery(`
|
||||
INSERT INTO calendar_events
|
||||
(google_event_id, user_id, title, description, start_time, end_time, location, html_link)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, [
|
||||
event.id,
|
||||
req.user.id,
|
||||
event.summary || 'Sans titre',
|
||||
event.description || '',
|
||||
startTime,
|
||||
endTime,
|
||||
event.location || '',
|
||||
event.htmlLink || ''
|
||||
]);
|
||||
}
|
||||
syncedCount++;
|
||||
}
|
||||
|
||||
logger.info(`${syncedCount} événements synchronisés pour l'utilisateur ${req.user.username}`);
|
||||
|
||||
res.json({
|
||||
message: 'Synchronisation réussie',
|
||||
syncedCount,
|
||||
events: events.length
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Erreur lors de la synchronisation avec Google Calendar:', error);
|
||||
res.status(500).json({
|
||||
error: 'Erreur lors de la synchronisation',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/calendar/events/:id
|
||||
* Supprimer un événement local
|
||||
*/
|
||||
router.delete('/events/:id', async (req, res) => {
|
||||
try {
|
||||
const event = await getOne(
|
||||
'SELECT id FROM calendar_events WHERE id = ? AND user_id = ?',
|
||||
[req.params.id, req.user.id]
|
||||
);
|
||||
|
||||
if (!event) {
|
||||
return res.status(404).json({ error: 'Événement non trouvé' });
|
||||
}
|
||||
|
||||
await runQuery('DELETE FROM calendar_events WHERE id = ?', [req.params.id]);
|
||||
|
||||
logger.info(`Événement ${req.params.id} supprimé`);
|
||||
|
||||
res.json({ message: 'Événement supprimé avec succès' });
|
||||
} catch (error) {
|
||||
logger.error('Erreur lors de la suppression de l\'événement:', error);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
+171
-1
@@ -58,7 +58,7 @@ router.get('/', async (req, res) => {
|
||||
ORDER BY n.updated_at DESC
|
||||
`, [req.user.id, archivedFilter]);
|
||||
|
||||
// Charger les todos et fichiers pour chaque note
|
||||
// Charger les todos, images et fichiers pour chaque note
|
||||
for (const note of notes) {
|
||||
const todos = await getAll(`
|
||||
SELECT id, text, completed, position
|
||||
@@ -68,6 +68,14 @@ router.get('/', async (req, res) => {
|
||||
`, [note.id]);
|
||||
note.todos = todos || [];
|
||||
|
||||
const images = await getAll(`
|
||||
SELECT id, filename, original_name, created_at
|
||||
FROM note_images
|
||||
WHERE note_id = ?
|
||||
ORDER BY created_at DESC
|
||||
`, [note.id]);
|
||||
note.images = images || [];
|
||||
|
||||
const files = await getAll(`
|
||||
SELECT id, filename, original_name, file_size, mime_type, created_at
|
||||
FROM note_files
|
||||
@@ -111,6 +119,26 @@ router.get('/:id', async (req, res) => {
|
||||
|
||||
note.todos = todos;
|
||||
|
||||
// Récupérer les images de la note
|
||||
const images = await getAll(`
|
||||
SELECT id, filename, original_name, created_at
|
||||
FROM note_images
|
||||
WHERE note_id = ?
|
||||
ORDER BY created_at DESC
|
||||
`, [req.params.id]);
|
||||
|
||||
note.images = images || [];
|
||||
|
||||
// Récupérer les fichiers de la note
|
||||
const files = await getAll(`
|
||||
SELECT id, filename, original_name, file_size, mime_type, created_at
|
||||
FROM note_files
|
||||
WHERE note_id = ?
|
||||
ORDER BY created_at DESC
|
||||
`, [req.params.id]);
|
||||
|
||||
note.files = files || [];
|
||||
|
||||
res.json(note);
|
||||
} catch (error) {
|
||||
logger.error('Erreur lors de la récupération de la note:', error);
|
||||
@@ -487,6 +515,148 @@ const searchNotes = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== IMAGE ATTACHMENTS ====================
|
||||
|
||||
// Configuration multer pour les images multiples
|
||||
const imageStorage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
const imagesDir = path.join(__dirname, '../public/uploads/images');
|
||||
// Créer le dossier s'il n'existe pas
|
||||
if (!fs.existsSync(imagesDir)) {
|
||||
fs.mkdirSync(imagesDir, { recursive: true });
|
||||
}
|
||||
cb(null, imagesDir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const uniqueName = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}${path.extname(file.originalname)}`;
|
||||
cb(null, uniqueName);
|
||||
}
|
||||
});
|
||||
|
||||
const imageUpload = multer({
|
||||
storage: imageStorage,
|
||||
limits: { fileSize: parseInt(process.env.MAX_FILE_SIZE) || 5 * 1024 * 1024 }, // 5MB
|
||||
fileFilter: (req, file, cb) => {
|
||||
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
|
||||
if (allowedTypes.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Type de fichier non autorisé. Utilisez JPEG, PNG, WebP ou GIF.'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/notes/:id/images
|
||||
* Ajouter une image à une note
|
||||
*/
|
||||
router.post('/:id/images', imageUpload.single('image'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'Aucune image fournie' });
|
||||
}
|
||||
|
||||
// Vérifier que la note appartient à l'utilisateur
|
||||
const note = await getOne('SELECT id FROM notes WHERE id = ? AND user_id = ?', [req.params.id, req.user.id]);
|
||||
if (!note) {
|
||||
// Supprimer le fichier uploadé
|
||||
fs.unlinkSync(req.file.path);
|
||||
return res.status(404).json({ error: 'Note non trouvée' });
|
||||
}
|
||||
|
||||
// Enregistrer l'image dans la base de données
|
||||
const result = await runQuery(`
|
||||
INSERT INTO note_images (note_id, filename, original_name)
|
||||
VALUES (?, ?, ?)
|
||||
`, [req.params.id, req.file.filename, req.file.originalname]);
|
||||
|
||||
// Mettre à jour la date de modification de la note
|
||||
await runQuery('UPDATE notes SET updated_at = CURRENT_TIMESTAMP WHERE id = ?', [req.params.id]);
|
||||
|
||||
logger.info(`Image ajoutée à la note ${req.params.id}: ${req.file.filename}`);
|
||||
|
||||
res.json({
|
||||
message: 'Image ajoutée avec succès',
|
||||
image: {
|
||||
id: result.id,
|
||||
filename: req.file.filename,
|
||||
original_name: req.file.originalname
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Erreur lors de l\'ajout de l\'image:', error);
|
||||
if (req.file && fs.existsSync(req.file.path)) {
|
||||
fs.unlinkSync(req.file.path);
|
||||
}
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/notes/:id/images
|
||||
* Liste les images d'une note
|
||||
*/
|
||||
router.get('/:id/images', async (req, res) => {
|
||||
try {
|
||||
// Vérifier que la note appartient à l'utilisateur
|
||||
const note = await getOne('SELECT id FROM notes WHERE id = ? AND user_id = ?', [req.params.id, req.user.id]);
|
||||
if (!note) {
|
||||
return res.status(404).json({ error: 'Note non trouvée' });
|
||||
}
|
||||
|
||||
const images = await getAll(`
|
||||
SELECT id, filename, original_name, created_at
|
||||
FROM note_images
|
||||
WHERE note_id = ?
|
||||
ORDER BY created_at DESC
|
||||
`, [req.params.id]);
|
||||
|
||||
res.json(images);
|
||||
} catch (error) {
|
||||
logger.error('Erreur lors de la récupération des images:', error);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/notes/:noteId/images/:imageId
|
||||
* Supprimer une image d'une note
|
||||
*/
|
||||
router.delete('/:noteId/images/:imageId', async (req, res) => {
|
||||
try {
|
||||
// Vérifier que l'image appartient à une note de l'utilisateur
|
||||
const image = await getOne(`
|
||||
SELECT ni.id, ni.filename, ni.note_id
|
||||
FROM note_images ni
|
||||
JOIN notes n ON ni.note_id = n.id
|
||||
WHERE ni.id = ? AND ni.note_id = ? AND n.user_id = ?
|
||||
`, [req.params.imageId, req.params.noteId, req.user.id]);
|
||||
|
||||
if (!image) {
|
||||
return res.status(404).json({ error: 'Image non trouvée' });
|
||||
}
|
||||
|
||||
// Supprimer le fichier physique
|
||||
const imagePath = path.join(__dirname, '../public/uploads/images', image.filename);
|
||||
if (fs.existsSync(imagePath)) {
|
||||
fs.unlinkSync(imagePath);
|
||||
}
|
||||
|
||||
// Supprimer l'entrée de la base de données
|
||||
await runQuery('DELETE FROM note_images WHERE id = ?', [req.params.imageId]);
|
||||
|
||||
// Mettre à jour la date de modification de la note
|
||||
await runQuery('UPDATE notes SET updated_at = CURRENT_TIMESTAMP WHERE id = ?', [req.params.noteId]);
|
||||
|
||||
logger.info(`Image ${image.filename} supprimée de la note ${req.params.noteId}`);
|
||||
|
||||
res.json({ message: 'Image supprimée avec succès' });
|
||||
} catch (error) {
|
||||
logger.error('Erreur lors de la suppression de l\'image:', error);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== FILE ATTACHMENTS ====================
|
||||
|
||||
// Configuration multer pour tous types de fichiers
|
||||
|
||||
@@ -86,6 +86,7 @@ app.use('/api/notes', require('./routes/notes.routes'));
|
||||
app.use('/api/todos', require('./routes/todos.routes'));
|
||||
app.use('/api/settings', require('./routes/settings.routes'));
|
||||
app.use('/api/rss', require('./routes/rss.routes'));
|
||||
app.use('/api/calendar', require('./routes/calendar.routes'));
|
||||
|
||||
// Route de recherche
|
||||
app.get('/api/search', require('./routes/notes.routes').searchNotes);
|
||||
|
||||
@@ -158,12 +158,15 @@ export function RichTextEditor({ content, onChange, placeholder }: RichTextEdito
|
||||
contentEditable
|
||||
onInput={updateContent}
|
||||
onKeyDown={handleKeyDown}
|
||||
dir="ltr"
|
||||
className="min-h-[400px] outline-none prose prose-sm max-w-none dark:prose-invert"
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
data-placeholder={placeholder || 'Commencez à écrire...'}
|
||||
style={{
|
||||
fontSize: '16px',
|
||||
lineHeight: '1.6'
|
||||
lineHeight: '1.6',
|
||||
direction: 'ltr',
|
||||
textAlign: 'left'
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
|
||||
+154
-3
@@ -13,7 +13,7 @@ import { RichTextEditor } from "@/components/RichTextEditor";
|
||||
import { MadeWithDyad } from "@/components/made-with-dyad";
|
||||
import {
|
||||
PlusCircle, Search, User, LogOut, Settings, ChevronDown, Plus, Archive, Trash2,
|
||||
Image as ImageIcon, CheckSquare, FileText, Rss, ExternalLink, RefreshCw, Key, Zap, Paperclip, X, Edit
|
||||
Image as ImageIcon, CheckSquare, FileText, Rss, ExternalLink, RefreshCw, Key, Zap, Paperclip, X, Edit, Calendar as CalendarIcon
|
||||
} from "lucide-react";
|
||||
import AuthService from "@/services/AuthService";
|
||||
import AdminService from "@/services/AdminService";
|
||||
@@ -21,6 +21,7 @@ import NotesService, { Note } from "@/services/NotesService";
|
||||
import TodosService, { Todo } from "@/services/TodosService";
|
||||
import RssService, { RssFeed, RssArticle } from "@/services/RssService";
|
||||
import SettingsService, { Settings as AppSettings } from "@/services/SettingsService";
|
||||
import CalendarService, { CalendarEvent } from "@/services/CalendarService";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { showError, showSuccess } from "@/utils/toast";
|
||||
|
||||
@@ -40,6 +41,7 @@ const Index = () => {
|
||||
const [todos, setTodos] = useState<Todo[]>([]);
|
||||
const [rssFeeds, setRssFeeds] = useState<RssFeed[]>([]);
|
||||
const [rssArticles, setRssArticles] = useState<RssArticle[]>([]);
|
||||
const [calendarEvents, setCalendarEvents] = useState<CalendarEvent[]>([]);
|
||||
const [settings, setSettings] = useState<AppSettings>({});
|
||||
const [adminTab, setAdminTab] = useState("users");
|
||||
const [showArchived, setShowArchived] = useState(false);
|
||||
@@ -53,6 +55,7 @@ const Index = () => {
|
||||
loadTodos();
|
||||
loadRssFeeds();
|
||||
loadRssArticles();
|
||||
loadCalendarEvents();
|
||||
} else {
|
||||
navigate("/login");
|
||||
}
|
||||
@@ -102,6 +105,17 @@ const Index = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadCalendarEvents = async () => {
|
||||
try {
|
||||
const events = await CalendarService.getEvents(10);
|
||||
if (Array.isArray(events)) {
|
||||
setCalendarEvents(events);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur lors du chargement des événements:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadUsers = async () => {
|
||||
if (user?.is_admin) {
|
||||
try {
|
||||
@@ -898,11 +912,80 @@ const Index = () => {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Calendar Box */}
|
||||
<Card className="shadow-lg">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-xl flex items-center gap-2">
|
||||
<CalendarIcon className="h-6 w-6" />
|
||||
Rendez-vous
|
||||
</CardTitle>
|
||||
<Button size="lg" variant="outline" onClick={async () => {
|
||||
try {
|
||||
const result = await CalendarService.sync();
|
||||
showSuccess(`${result.syncedCount} événements synchronisés`);
|
||||
await loadCalendarEvents();
|
||||
} catch (error) {
|
||||
showError("Erreur lors de la synchronisation");
|
||||
}
|
||||
}} className="gap-2">
|
||||
<RefreshCw className="h-5 w-5" />
|
||||
Synchroniser
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3 max-h-[480px] overflow-y-auto">
|
||||
{calendarEvents.length > 0 ? (
|
||||
calendarEvents.map(event => {
|
||||
const startDate = new Date(event.start_time);
|
||||
const endDate = new Date(event.end_time);
|
||||
const isToday = startDate.toDateString() === new Date().toDateString();
|
||||
|
||||
return (
|
||||
<div
|
||||
key={event.id}
|
||||
className="p-3 border rounded-lg hover:bg-accent/50 transition-colors cursor-pointer"
|
||||
onClick={() => event.html_link && window.open(event.html_link, '_blank')}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-medium line-clamp-2 mb-1">{event.title}</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isToday ? "Aujourd'hui" : startDate.toLocaleDateString('fr-FR')} à {startDate.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
|
||||
</p>
|
||||
{event.location && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
📍 {event.location}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<ExternalLink className="h-4 w-4 text-muted-foreground flex-shrink-0 mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="text-center text-muted-foreground py-8">
|
||||
Aucun rendez-vous à venir
|
||||
<br />
|
||||
<Button
|
||||
variant="link"
|
||||
className="mt-2"
|
||||
onClick={() => setShowAdmin(true)}
|
||||
>
|
||||
Configurer Google Calendar
|
||||
</Button>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Admin Modal */}
|
||||
{/* Admin Modal */}
|
||||
<Dialog open={showAdmin} onOpenChange={setShowAdmin}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
@@ -914,9 +997,10 @@ const Index = () => {
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs value={adminTab} onValueChange={setAdminTab}>
|
||||
<TabsList className="grid grid-cols-4 w-full">
|
||||
<TabsList className="grid grid-cols-5 w-full">
|
||||
<TabsTrigger value="users">Utilisateurs</TabsTrigger>
|
||||
<TabsTrigger value="rss">Flux RSS</TabsTrigger>
|
||||
<TabsTrigger value="calendar">Google Calendar</TabsTrigger>
|
||||
<TabsTrigger value="openrouter">OpenRouter</TabsTrigger>
|
||||
<TabsTrigger value="settings">Paramètres</TabsTrigger>
|
||||
</TabsList>
|
||||
@@ -1003,6 +1087,73 @@ const Index = () => {
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="calendar" className="mt-4 space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<CalendarIcon className="h-5 w-5" />
|
||||
Configuration Google Calendar
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="calendar-api-key">Clé API Google Calendar</Label>
|
||||
<Input
|
||||
id="calendar-api-key"
|
||||
type="password"
|
||||
placeholder="AIza..."
|
||||
value={settings.google_calendar_api_key || ''}
|
||||
onChange={(e) => setSettings({ ...settings, google_calendar_api_key: e.target.value })}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Créez une clé API sur{' '}
|
||||
<a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
|
||||
Google Cloud Console
|
||||
</a>
|
||||
{' '}et activez l'API Google Calendar
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="calendar-id">ID du calendrier</Label>
|
||||
<Input
|
||||
id="calendar-id"
|
||||
placeholder="votre-email@gmail.com ou id-calendrier"
|
||||
value={settings.google_calendar_id || ''}
|
||||
onChange={(e) => setSettings({ ...settings, google_calendar_id: e.target.value })}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Trouvez l'ID du calendrier dans les paramètres de votre calendrier Google.<br />
|
||||
Pour votre calendrier principal, utilisez votre adresse email.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleSaveSettings} className="flex items-center gap-2">
|
||||
<CalendarIcon className="h-4 w-4" />
|
||||
Enregistrer
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const result = await CalendarService.sync();
|
||||
showSuccess(`${result.syncedCount} événements synchronisés`);
|
||||
await loadCalendarEvents();
|
||||
} catch (error) {
|
||||
showError("Erreur lors de la synchronisation. Vérifiez votre configuration.");
|
||||
}
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Tester la synchronisation
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="openrouter" className="mt-4 space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import axios from "axios";
|
||||
import AuthService from "./AuthService";
|
||||
|
||||
export interface CalendarEvent {
|
||||
id: number;
|
||||
google_event_id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
location?: string;
|
||||
html_link?: string;
|
||||
synced_at?: string;
|
||||
}
|
||||
|
||||
class CalendarService {
|
||||
private baseURL = "/api/calendar";
|
||||
|
||||
private getAuthHeader() {
|
||||
return {
|
||||
headers: {
|
||||
Authorization: `Bearer ${AuthService.getToken()}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupérer les événements du calendrier
|
||||
*/
|
||||
async getEvents(limit: number = 10): Promise<CalendarEvent[]> {
|
||||
try {
|
||||
const response = await axios.get(`${this.baseURL}/events?limit=${limit}`, this.getAuthHeader());
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la récupération des événements:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchroniser avec Google Calendar
|
||||
*/
|
||||
async sync(): Promise<{ syncedCount: number; events: number }> {
|
||||
const response = await axios.post(`${this.baseURL}/sync`, {}, this.getAuthHeader());
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprimer un événement local
|
||||
*/
|
||||
async deleteEvent(eventId: number): Promise<boolean> {
|
||||
try {
|
||||
await axios.delete(`${this.baseURL}/events/${eventId}`, this.getAuthHeader());
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la suppression de l'événement:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new CalendarService();
|
||||
@@ -5,6 +5,8 @@ export interface Settings {
|
||||
ai_model?: string;
|
||||
rss_refresh_interval?: number;
|
||||
theme?: 'light' | 'dark' | 'auto';
|
||||
google_calendar_api_key?: string;
|
||||
google_calendar_id?: string;
|
||||
}
|
||||
|
||||
class SettingsService {
|
||||
|
||||
Reference in new issue
Block a user