mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-23 15:18:16 +02:00
Merge pull request #61 from R0m1k3/claude/integrate-google-calendar-api-011CV44Nie9HB7U35ArSUnNa
Claude/integrate google calendar api 011 cv44 nie9 hb7 u35 ar s un na
This commit is contained in:
4 files changed
+451
-259
No files matched your search
@@ -286,6 +286,27 @@ function initDatabase() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Migration pour calendar_events - ajouter all_day
|
||||||
|
db.all("PRAGMA table_info(calendar_events)", (err, columns) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error('Erreur lors de la vérification de la structure de la table calendar_events:', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAllDay = columns.some(col => col.name === 'all_day');
|
||||||
|
|
||||||
|
if (!hasAllDay) {
|
||||||
|
logger.info('Migration: Ajout de la colonne all_day à calendar_events...');
|
||||||
|
db.run('ALTER TABLE calendar_events ADD COLUMN all_day BOOLEAN DEFAULT 0', (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error('Erreur lors de l\'ajout de la colonne all_day:', err);
|
||||||
|
} else {
|
||||||
|
logger.info('✓ Colonne all_day ajoutée avec succès');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Créer l'utilisateur admin par défaut si la table est vide
|
// Créer l'utilisateur admin par défaut si la table est vide
|
||||||
db.get('SELECT COUNT(*) as count FROM users', async (err, row) => {
|
db.get('SELECT COUNT(*) as count FROM users', async (err, row) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|||||||
+121
-6
@@ -33,14 +33,19 @@ async function getAuthType() {
|
|||||||
async function getOAuth2Client(customRedirectUri = null) {
|
async function getOAuth2Client(customRedirectUri = null) {
|
||||||
const clientId = await getOne("SELECT value FROM settings WHERE key = 'google_client_id'");
|
const clientId = await getOne("SELECT value FROM settings WHERE key = 'google_client_id'");
|
||||||
const clientSecret = await getOne("SELECT value FROM settings WHERE key = 'google_client_secret'");
|
const clientSecret = await getOne("SELECT value FROM settings WHERE key = 'google_client_secret'");
|
||||||
|
const appUrl = await getOne("SELECT value FROM settings WHERE key = 'app_url'");
|
||||||
|
|
||||||
if (!clientId || !clientId.value || !clientSecret || !clientSecret.value) {
|
if (!clientId || !clientId.value || !clientSecret || !clientSecret.value) {
|
||||||
throw new Error('Client ID et Client Secret non configurés');
|
throw new Error('Client ID et Client Secret non configurés');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utiliser l'URI personnalisée si fournie, sinon utiliser l'URL de production
|
// Utiliser l'URI personnalisée si fournie, sinon utiliser l'URL du site configurée
|
||||||
const redirectUri = customRedirectUri ||
|
const redirectUri = customRedirectUri ||
|
||||||
'https://note.ffnancy.fr/api/calendar/oauth-callback';
|
(appUrl?.value ? `${appUrl.value}/api/calendar/oauth-callback` : null);
|
||||||
|
|
||||||
|
if (!redirectUri) {
|
||||||
|
throw new Error('URL du site non configurée. Veuillez configurer l\'URL dans les paramètres.');
|
||||||
|
}
|
||||||
|
|
||||||
return new google.auth.OAuth2(
|
return new google.auth.OAuth2(
|
||||||
clientId.value,
|
clientId.value,
|
||||||
@@ -412,6 +417,7 @@ router.post('/sync', authenticateToken, async (req, res) => {
|
|||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
const startTime = event.start.dateTime || event.start.date;
|
const startTime = event.start.dateTime || event.start.date;
|
||||||
const endTime = event.end.dateTime || event.end.date;
|
const endTime = event.end.dateTime || event.end.date;
|
||||||
|
const isAllDay = !event.start.dateTime; // Si pas de dateTime, c'est un événement toute la journée
|
||||||
|
|
||||||
// Vérifier si l'événement existe déjà
|
// Vérifier si l'événement existe déjà
|
||||||
const existing = await getOne(
|
const existing = await getOne(
|
||||||
@@ -424,7 +430,7 @@ router.post('/sync', authenticateToken, async (req, res) => {
|
|||||||
await runQuery(`
|
await runQuery(`
|
||||||
UPDATE calendar_events
|
UPDATE calendar_events
|
||||||
SET title = ?, description = ?, start_time = ?, end_time = ?,
|
SET title = ?, description = ?, start_time = ?, end_time = ?,
|
||||||
location = ?, html_link = ?, synced_at = CURRENT_TIMESTAMP
|
location = ?, html_link = ?, all_day = ?, synced_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`, [
|
`, [
|
||||||
event.summary || 'Sans titre',
|
event.summary || 'Sans titre',
|
||||||
@@ -433,14 +439,15 @@ router.post('/sync', authenticateToken, async (req, res) => {
|
|||||||
endTime,
|
endTime,
|
||||||
event.location || '',
|
event.location || '',
|
||||||
event.htmlLink || '',
|
event.htmlLink || '',
|
||||||
|
isAllDay ? 1 : 0,
|
||||||
existing.id
|
existing.id
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
// Créer un nouvel événement
|
// Créer un nouvel événement
|
||||||
await runQuery(`
|
await runQuery(`
|
||||||
INSERT INTO calendar_events
|
INSERT INTO calendar_events
|
||||||
(google_event_id, user_id, title, description, start_time, end_time, location, html_link)
|
(google_event_id, user_id, title, description, start_time, end_time, location, html_link, all_day)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`, [
|
`, [
|
||||||
event.id,
|
event.id,
|
||||||
req.user.id,
|
req.user.id,
|
||||||
@@ -449,7 +456,8 @@ router.post('/sync', authenticateToken, async (req, res) => {
|
|||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
event.location || '',
|
event.location || '',
|
||||||
event.htmlLink || ''
|
event.htmlLink || '',
|
||||||
|
isAllDay ? 1 : 0
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
syncedCount++;
|
syncedCount++;
|
||||||
@@ -606,6 +614,113 @@ router.post('/events', authenticateToken, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /api/calendar/events/:id
|
||||||
|
* Mettre à jour un événement dans Google Calendar
|
||||||
|
*/
|
||||||
|
router.put('/events/:id', authenticateToken, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
startDateTime,
|
||||||
|
endDateTime,
|
||||||
|
location,
|
||||||
|
attendees,
|
||||||
|
reminders,
|
||||||
|
recurrence,
|
||||||
|
visibility,
|
||||||
|
colorId
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// Récupérer l'événement existant depuis la DB
|
||||||
|
const dbEvent = await getOne(
|
||||||
|
'SELECT google_event_id FROM calendar_events WHERE id = ? AND user_id = ?',
|
||||||
|
[req.params.id, req.user.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!dbEvent) {
|
||||||
|
return res.status(404).json({ error: 'Événement non trouvé' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if (!title || !startDateTime || !endDateTime) {
|
||||||
|
return res.status(400).json({ error: 'Titre, date de début et date de fin requis' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const calendar = await getCalendarClient(req.user.id);
|
||||||
|
|
||||||
|
// Préparer l'événement mis à jour
|
||||||
|
const event = {
|
||||||
|
summary: title,
|
||||||
|
location: location || undefined,
|
||||||
|
description: description || undefined,
|
||||||
|
start: {
|
||||||
|
dateTime: startDateTime,
|
||||||
|
timeZone: 'Europe/Paris',
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
dateTime: endDateTime,
|
||||||
|
timeZone: 'Europe/Paris',
|
||||||
|
},
|
||||||
|
attendees: attendees && attendees.length > 0 ?
|
||||||
|
attendees.map(email => ({ email })) : undefined,
|
||||||
|
reminders: reminders ? {
|
||||||
|
useDefault: false,
|
||||||
|
overrides: reminders
|
||||||
|
} : undefined,
|
||||||
|
recurrence: recurrence || undefined,
|
||||||
|
visibility: visibility || 'default',
|
||||||
|
colorId: colorId || undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
// Récupérer le Calendar ID depuis les settings
|
||||||
|
const calendarIdSetting = await getOne("SELECT value FROM settings WHERE key = 'google_calendar_id'");
|
||||||
|
const calendarId = calendarIdSetting?.value || 'primary';
|
||||||
|
|
||||||
|
// Mettre à jour l'événement dans Google Calendar
|
||||||
|
const response = await calendar.events.update({
|
||||||
|
calendarId: calendarId,
|
||||||
|
eventId: dbEvent.google_event_id,
|
||||||
|
resource: event,
|
||||||
|
sendUpdates: attendees && attendees.length > 0 ? 'all' : 'none'
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`Événement mis à jour: ${response.data.id} pour l'utilisateur ${req.user.username}`);
|
||||||
|
|
||||||
|
// Mettre à jour dans la base de données locale
|
||||||
|
const isAllDay = !startDateTime.includes('T') && !endDateTime.includes('T');
|
||||||
|
await runQuery(`
|
||||||
|
UPDATE calendar_events
|
||||||
|
SET title = ?, description = ?, start_time = ?, end_time = ?,
|
||||||
|
location = ?, html_link = ?, all_day = ?, synced_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?
|
||||||
|
`, [
|
||||||
|
title,
|
||||||
|
description || null,
|
||||||
|
startDateTime,
|
||||||
|
endDateTime,
|
||||||
|
location || null,
|
||||||
|
response.data.htmlLink || null,
|
||||||
|
isAllDay ? 1 : 0,
|
||||||
|
req.params.id
|
||||||
|
]);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'Événement mis à jour avec succès',
|
||||||
|
eventId: response.data.id,
|
||||||
|
htmlLink: response.data.htmlLink
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Erreur lors de la mise à jour de l\'événement:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Erreur lors de la mise à jour de l\'événement',
|
||||||
|
details: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE /api/calendar/events/:id
|
* DELETE /api/calendar/events/:id
|
||||||
* Supprimer un événement local
|
* Supprimer un événement local
|
||||||
|
|||||||
+289
-253
@@ -78,6 +78,7 @@ const Index = () => {
|
|||||||
const [addTagModal, setAddTagModal] = useState(false);
|
const [addTagModal, setAddTagModal] = useState(false);
|
||||||
const [selectedImage, setSelectedImage] = useState<string | null>(null);
|
const [selectedImage, setSelectedImage] = useState<string | null>(null);
|
||||||
const [addEventModal, setAddEventModal] = useState(false);
|
const [addEventModal, setAddEventModal] = useState(false);
|
||||||
|
const [editEventModal, setEditEventModal] = useState<{open: boolean, event?: CalendarEvent}>({open: false});
|
||||||
|
|
||||||
// Pagination states
|
// Pagination states
|
||||||
const [notesPage, setNotesPage] = useState(0);
|
const [notesPage, setNotesPage] = useState(0);
|
||||||
@@ -97,7 +98,6 @@ const Index = () => {
|
|||||||
const [chatLoading, setChatLoading] = useState(false);
|
const [chatLoading, setChatLoading] = useState(false);
|
||||||
const [selectedModel, setSelectedModel] = useState<string>("");
|
const [selectedModel, setSelectedModel] = useState<string>("");
|
||||||
const [defaultModel, setDefaultModel] = useState<string>("");
|
const [defaultModel, setDefaultModel] = useState<string>("");
|
||||||
const [modelSearchQuery, setModelSearchQuery] = useState<string>("");
|
|
||||||
|
|
||||||
// Debounce timer for auto-save
|
// Debounce timer for auto-save
|
||||||
const saveTimerRef = useRef<NodeJS.Timeout | null>(null);
|
const saveTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
@@ -856,11 +856,13 @@ const Index = () => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={event.id}
|
key={event.id}
|
||||||
className={`p-3 border rounded-lg hover:bg-accent/50 transition-colors cursor-pointer ${isSoon ? 'border-red-500 bg-red-50 dark:bg-red-900/20' : ''}`}
|
className={`p-3 border rounded-lg hover:bg-accent/50 transition-colors group relative ${isSoon ? 'border-red-500 bg-red-50 dark:bg-red-900/20' : ''}`}
|
||||||
onClick={() => event.html_link && window.open(event.html_link, '_blank')}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<div className="flex-1 min-w-0">
|
<div
|
||||||
|
className="flex-1 min-w-0 cursor-pointer"
|
||||||
|
onClick={() => event.html_link && window.open(event.html_link, '_blank')}
|
||||||
|
>
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
{isToday && (
|
{isToday && (
|
||||||
<div className="h-2 w-2 rounded-full bg-blue-500 flex-shrink-0 animate-pulse" />
|
<div className="h-2 w-2 rounded-full bg-blue-500 flex-shrink-0 animate-pulse" />
|
||||||
@@ -868,7 +870,8 @@ const Index = () => {
|
|||||||
<h4 className="font-medium line-clamp-2 text-sm">{event.title}</h4>
|
<h4 className="font-medium line-clamp-2 text-sm">{event.title}</h4>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{isToday ? "Aujourd'hui" : startDate.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit' })} à {startDate.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
|
{isToday ? "Aujourd'hui" : startDate.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit' })}
|
||||||
|
{event.all_day ? ' - Toute la journée' : ` à ${startDate.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}`}
|
||||||
</p>
|
</p>
|
||||||
{event.location && (
|
{event.location && (
|
||||||
<p className="text-xs text-muted-foreground mt-1 truncate">
|
<p className="text-xs text-muted-foreground mt-1 truncate">
|
||||||
@@ -879,7 +882,23 @@ const Index = () => {
|
|||||||
<Badge variant="destructive" className="mt-1 text-xs">Dans moins de 30 min</Badge>
|
<Badge variant="destructive" className="mt-1 text-xs">Dans moins de 30 min</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ExternalLink className="h-3 w-3 text-muted-foreground flex-shrink-0 mt-1" />
|
<div className="flex gap-1 items-start">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setEditEventModal({ open: true, event });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Edit className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
<ExternalLink
|
||||||
|
className="h-3 w-3 text-muted-foreground flex-shrink-0 mt-1 cursor-pointer"
|
||||||
|
onClick={() => event.html_link && window.open(event.html_link, '_blank')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1691,182 +1710,63 @@ const Index = () => {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<CalendarIcon className="h-5 w-5" />
|
<CalendarIcon className="h-5 w-5" />
|
||||||
Configuration Google Calendar
|
Configuration Google Calendar OAuth 2.0
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{/* Choix du type d'authentification */}
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="google-auth-type">Méthode d'authentification</Label>
|
<Label htmlFor="app-url">URL du site</Label>
|
||||||
<Select
|
<Input
|
||||||
value={settings.google_auth_type || 'oauth2'}
|
id="app-url"
|
||||||
onValueChange={(value) => setSettings({ ...settings, google_auth_type: value as 'oauth2' | 'service_account' | 'api_externe' })}
|
type="url"
|
||||||
>
|
placeholder="https://votre-domaine.com"
|
||||||
<SelectTrigger id="google-auth-type">
|
value={settings.app_url || ''}
|
||||||
<SelectValue placeholder="Sélectionnez une méthode" />
|
onChange={(e) => setSettings({ ...settings, app_url: e.target.value })}
|
||||||
</SelectTrigger>
|
/>
|
||||||
<SelectContent>
|
<p className="text-sm text-muted-foreground">
|
||||||
<SelectItem value="oauth2">
|
URL de votre site (utilisée pour les redirections OAuth)
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="font-medium">OAuth 2.0</span>
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
Calendrier personnel avec consentement utilisateur
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="service_account">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="font-medium">Service Account</span>
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
Clé JSON pour calendriers partagés (sans interaction)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="api_externe">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="font-medium">API externe</span>
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
Connectez-vous à une API externe pour Google Calendar
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
<strong>OAuth 2.0</strong> : Pour accéder au calendrier personnel avec autorisation de l'utilisateur.<br/>
|
|
||||||
<strong>Service Account</strong> : Pour accéder à des calendriers partagés sans interaction utilisateur.<br/>
|
|
||||||
<strong>API externe</strong> : Utilisez votre propre API backend pour gérer la connexion à Google Calendar.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Configuration OAuth2 */}
|
<div className="space-y-2">
|
||||||
{(!settings.google_auth_type || settings.google_auth_type === 'oauth2') && (
|
<Label htmlFor="google-client-id">Client ID Google</Label>
|
||||||
<>
|
<Input
|
||||||
<div className="space-y-2">
|
id="google-client-id"
|
||||||
<Label htmlFor="google-client-id">Client ID Google</Label>
|
type="text"
|
||||||
<Input
|
placeholder="123456789-abcdef.apps.googleusercontent.com"
|
||||||
id="google-client-id"
|
value={settings.google_client_id || ''}
|
||||||
type="text"
|
onChange={(e) => setSettings({ ...settings, google_client_id: e.target.value })}
|
||||||
placeholder="123456789-abcdef.apps.googleusercontent.com"
|
/>
|
||||||
value={settings.google_client_id || ''}
|
</div>
|
||||||
onChange={(e) => setSettings({ ...settings, google_client_id: e.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="google-client-secret">Client Secret Google</Label>
|
<Label htmlFor="google-client-secret">Client Secret Google</Label>
|
||||||
<Input
|
<Input
|
||||||
id="google-client-secret"
|
id="google-client-secret"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="GOCSPX-..."
|
placeholder="GOCSPX-..."
|
||||||
value={settings.google_client_secret || ''}
|
value={settings.google_client_secret || ''}
|
||||||
onChange={(e) => setSettings({ ...settings, google_client_secret: e.target.value })}
|
onChange={(e) => setSettings({ ...settings, google_client_secret: e.target.value })}
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Créez un projet OAuth 2.0 sur{' '}
|
Créez un projet OAuth 2.0 sur{' '}
|
||||||
<a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
|
<a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
|
||||||
Google Cloud Console
|
Google Cloud Console
|
||||||
</a>
|
</a>
|
||||||
{' '}et activez l'API Google Calendar.
|
{' '}et activez l'API Google Calendar.
|
||||||
</p>
|
</p>
|
||||||
<div className="p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
|
<div className="p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
|
||||||
<p className="text-sm font-semibold text-yellow-900 dark:text-yellow-200 mb-1">
|
<p className="text-sm font-semibold text-yellow-900 dark:text-yellow-200 mb-1">
|
||||||
⚠️ IMPORTANT : URI de redirection autorisée
|
⚠️ IMPORTANT : URI de redirection autorisée
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-yellow-800 dark:text-yellow-300 mb-2">
|
<p className="text-xs text-yellow-800 dark:text-yellow-300 mb-2">
|
||||||
Ajoutez cette URL exacte dans la console Google Cloud (OAuth 2.0 Client IDs → URIs de redirection autorisées) :
|
Ajoutez cette URL exacte dans la console Google Cloud (OAuth 2.0 Client IDs → URIs de redirection autorisées) :
|
||||||
</p>
|
</p>
|
||||||
<code className="block p-2 bg-white dark:bg-gray-800 border border-yellow-300 dark:border-yellow-700 rounded text-xs font-mono break-all">
|
<code className="block p-2 bg-white dark:bg-gray-800 border border-yellow-300 dark:border-yellow-700 rounded text-xs font-mono break-all">
|
||||||
{window.location.origin}/api/calendar/oauth-callback
|
{settings.app_url || window.location.origin}/api/calendar/oauth-callback
|
||||||
</code>
|
</code>
|
||||||
<p className="text-xs text-yellow-800 dark:text-yellow-300 mt-2">
|
</div>
|
||||||
💡 Si vous utilisez un domaine différent, créez un fichier .env avec APP_URL=https://votre-domaine.com
|
</div>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Configuration Service Account */}
|
|
||||||
{settings.google_auth_type === 'service_account' && (
|
|
||||||
<>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="google-service-account-key">Clé Service Account (JSON)</Label>
|
|
||||||
<textarea
|
|
||||||
id="google-service-account-key"
|
|
||||||
className="w-full min-h-[200px] p-3 text-xs font-mono border rounded-md"
|
|
||||||
placeholder='Collez ici le contenu du fichier JSON de votre Service Account { "type": "service_account", "project_id": "...", "private_key_id": "...", ... }'
|
|
||||||
value={settings.google_service_account_key || ''}
|
|
||||||
onChange={(e) => setSettings({ ...settings, google_service_account_key: e.target.value })}
|
|
||||||
/>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Créez un Service Account sur{' '}
|
|
||||||
<a href="https://console.cloud.google.com/iam-admin/serviceaccounts" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
|
|
||||||
Google Cloud Console
|
|
||||||
</a>
|
|
||||||
{' '}et téléchargez la clé JSON.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="google-calendar-email">Email du calendrier à synchroniser</Label>
|
|
||||||
<Input
|
|
||||||
id="google-calendar-email"
|
|
||||||
type="email"
|
|
||||||
placeholder="votre-email@gmail.com"
|
|
||||||
value={settings.google_calendar_email || ''}
|
|
||||||
onChange={(e) => setSettings({ ...settings, google_calendar_email: e.target.value })}
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
⚠️ Important : Vous devez partager votre calendrier avec l'email du Service Account (visible dans le JSON)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Configuration API Key */}
|
|
||||||
{settings.google_auth_type === 'api_externe' && (
|
|
||||||
<>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="google-api-key">Clé API Google Calendar</Label>
|
|
||||||
<Input
|
|
||||||
id="google-api-key"
|
|
||||||
type="password"
|
|
||||||
placeholder="AIzaSy..."
|
|
||||||
value={settings.google_calendar_api_key || ''}
|
|
||||||
onChange={(e) => setSettings({ ...settings, google_calendar_api_key: e.target.value })}
|
|
||||||
/>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Entrez votre clé API Google Calendar.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="google-calendar-id-api">ID du calendrier</Label>
|
|
||||||
<Input
|
|
||||||
id="google-calendar-id-api"
|
|
||||||
type="text"
|
|
||||||
placeholder="primary ou votre-email@gmail.com"
|
|
||||||
value={settings.google_calendar_id || ''}
|
|
||||||
onChange={(e) => setSettings({ ...settings, google_calendar_id: e.target.value })}
|
|
||||||
/>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Utilisez "primary" pour votre calendrier principal ou l'email du calendrier spécifique.
|
|
||||||
</p>
|
|
||||||
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
|
||||||
<p className="text-sm font-semibold text-blue-900 dark:text-blue-200 mb-1">
|
|
||||||
ℹ️ Obtenir une clé API
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-blue-800 dark:text-blue-300 mb-2">
|
|
||||||
1. Allez sur <a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener noreferrer" className="underline">Google Cloud Console</a><br/>
|
|
||||||
2. Créez une clé API<br/>
|
|
||||||
3. Activez l'API Google Calendar<br/>
|
|
||||||
4. Copiez la clé API ici
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button onClick={handleSaveSettings} className="flex items-center gap-2">
|
<Button onClick={handleSaveSettings} className="flex items-center gap-2">
|
||||||
@@ -1875,22 +1775,14 @@ const Index = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(
|
{settings.google_client_id && settings.google_client_secret && (
|
||||||
((!settings.google_auth_type || settings.google_auth_type === 'oauth2') && settings.google_client_id && settings.google_client_secret) ||
|
|
||||||
(settings.google_auth_type === 'service_account' && settings.google_service_account_key) ||
|
|
||||||
(settings.google_auth_type === 'api_externe' && settings.google_calendar_api_key)
|
|
||||||
) && (
|
|
||||||
<>
|
<>
|
||||||
<div className="border-t pt-4">
|
<div className="border-t pt-4">
|
||||||
<h3 className="font-semibold mb-2">Statut de connexion</h3>
|
<h3 className="font-semibold mb-2">Statut de connexion</h3>
|
||||||
{calendarAuthStatus.isAuthenticated && !calendarAuthStatus.isExpired ? (
|
{calendarAuthStatus.isAuthenticated && !calendarAuthStatus.isExpired ? (
|
||||||
<div className="flex items-center gap-2 text-green-600">
|
<div className="flex items-center gap-2 text-green-600">
|
||||||
<div className="h-2 w-2 rounded-full bg-green-600"></div>
|
<div className="h-2 w-2 rounded-full bg-green-600"></div>
|
||||||
<span>Connecté à Google Calendar ({
|
<span>Connecté à Google Calendar (OAuth 2.0)</span>
|
||||||
calendarAuthStatus.authType === 'service_account' ? 'Service Account' :
|
|
||||||
calendarAuthStatus.authType === 'api_externe' ? 'API Key' :
|
|
||||||
'OAuth 2.0'
|
|
||||||
})</span>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-2 text-yellow-600">
|
<div className="flex items-center gap-2 text-yellow-600">
|
||||||
@@ -1901,12 +1793,12 @@ const Index = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{calendarAuthStatus.needsReauth && (!settings.google_auth_type || settings.google_auth_type === 'oauth2') ? (
|
{calendarAuthStatus.needsReauth ? (
|
||||||
<Button onClick={handleGoogleConnect} className="flex items-center gap-2">
|
<Button onClick={handleGoogleConnect} className="flex items-center gap-2">
|
||||||
<CalendarIcon className="h-4 w-4" />
|
<CalendarIcon className="h-4 w-4" />
|
||||||
Se connecter avec Google
|
Se connecter avec Google
|
||||||
</Button>
|
</Button>
|
||||||
) : !calendarAuthStatus.needsReauth && (!settings.google_auth_type || settings.google_auth_type === 'oauth2') ? (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
@@ -1937,23 +1829,7 @@ const Index = () => {
|
|||||||
Déconnecter
|
Déconnecter
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : settings.google_auth_type === 'api_externe' || settings.google_auth_type === 'service_account' ? (
|
)}
|
||||||
<Button
|
|
||||||
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="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<RefreshCw className="h-4 w-4" />
|
|
||||||
Synchroniser
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -2286,6 +2162,185 @@ const Index = () => {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Edit Event Modal */}
|
||||||
|
<Dialog open={editEventModal.open} onOpenChange={(open) => setEditEventModal({open, event: undefined})}>
|
||||||
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Modifier le rendez-vous</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Modifier l'événement dans votre Google Calendar
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<form onSubmit={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!editEventModal.event?.id) return;
|
||||||
|
|
||||||
|
const formData = new FormData(e.currentTarget);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const reminderMinutes = formData.get('reminderMinutes') as string;
|
||||||
|
const recurrenceValue = formData.get('recurrence') as string;
|
||||||
|
|
||||||
|
const eventData = {
|
||||||
|
title: formData.get('title') as string,
|
||||||
|
description: formData.get('description') as string || undefined,
|
||||||
|
startDateTime: new Date(formData.get('startDateTime') as string).toISOString(),
|
||||||
|
endDateTime: new Date(formData.get('endDateTime') as string).toISOString(),
|
||||||
|
location: formData.get('location') as string || undefined,
|
||||||
|
attendees: (formData.get('attendees') as string || '').split(',').map(e => e.trim()).filter(e => e),
|
||||||
|
reminders: reminderMinutes && reminderMinutes !== '0' ? [
|
||||||
|
{ method: 'popup', minutes: parseInt(reminderMinutes) }
|
||||||
|
] : undefined,
|
||||||
|
recurrence: recurrenceValue ? [recurrenceValue] : undefined,
|
||||||
|
visibility: formData.get('visibility') as string || 'default',
|
||||||
|
colorId: formData.get('colorId') as string || undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
await CalendarService.updateEvent(editEventModal.event.id, eventData);
|
||||||
|
showSuccess('Événement mis à jour avec succès');
|
||||||
|
setEditEventModal({open: false, event: undefined});
|
||||||
|
await loadCalendarEvents();
|
||||||
|
} catch (error: any) {
|
||||||
|
showError(error.response?.data?.error || 'Erreur lors de la mise à jour de l\'événement');
|
||||||
|
}
|
||||||
|
}} className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-title">Titre *</Label>
|
||||||
|
<Input id="edit-title" name="title" required defaultValue={editEventModal.event?.title || ''} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-description">Description</Label>
|
||||||
|
<textarea
|
||||||
|
id="edit-description"
|
||||||
|
name="description"
|
||||||
|
className="w-full min-h-[80px] px-3 py-2 border border-input rounded-md bg-background"
|
||||||
|
defaultValue={editEventModal.event?.description || ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-startDateTime">Début *</Label>
|
||||||
|
<Input
|
||||||
|
id="edit-startDateTime"
|
||||||
|
name="startDateTime"
|
||||||
|
type="datetime-local"
|
||||||
|
required
|
||||||
|
defaultValue={editEventModal.event ? new Date(editEventModal.event.start_time).toISOString().slice(0, 16) : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-endDateTime">Fin *</Label>
|
||||||
|
<Input
|
||||||
|
id="edit-endDateTime"
|
||||||
|
name="endDateTime"
|
||||||
|
type="datetime-local"
|
||||||
|
required
|
||||||
|
defaultValue={editEventModal.event ? new Date(editEventModal.event.end_time).toISOString().slice(0, 16) : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-location">Lieu</Label>
|
||||||
|
<Input id="edit-location" name="location" defaultValue={editEventModal.event?.location || ''} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-attendees">Participants (emails séparés par des virgules)</Label>
|
||||||
|
<Input
|
||||||
|
id="edit-attendees"
|
||||||
|
name="attendees"
|
||||||
|
type="text"
|
||||||
|
placeholder="email1@example.com, email2@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-reminderMinutes">Rappel (minutes avant)</Label>
|
||||||
|
<select
|
||||||
|
id="edit-reminderMinutes"
|
||||||
|
name="reminderMinutes"
|
||||||
|
className="w-full h-10 px-3 py-2 border border-input rounded-md bg-background"
|
||||||
|
defaultValue="30"
|
||||||
|
>
|
||||||
|
<option value="0">Aucun</option>
|
||||||
|
<option value="10">10 minutes</option>
|
||||||
|
<option value="15">15 minutes</option>
|
||||||
|
<option value="30">30 minutes</option>
|
||||||
|
<option value="60">1 heure</option>
|
||||||
|
<option value="120">2 heures</option>
|
||||||
|
<option value="1440">1 jour</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-recurrence">Récurrence</Label>
|
||||||
|
<select
|
||||||
|
id="edit-recurrence"
|
||||||
|
name="recurrence"
|
||||||
|
className="w-full h-10 px-3 py-2 border border-input rounded-md bg-background"
|
||||||
|
defaultValue=""
|
||||||
|
>
|
||||||
|
<option value="">Aucune</option>
|
||||||
|
<option value="RRULE:FREQ=DAILY">Quotidienne</option>
|
||||||
|
<option value="RRULE:FREQ=WEEKLY">Hebdomadaire</option>
|
||||||
|
<option value="RRULE:FREQ=MONTHLY">Mensuelle</option>
|
||||||
|
<option value="RRULE:FREQ=YEARLY">Annuelle</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-visibility">Visibilité</Label>
|
||||||
|
<select
|
||||||
|
id="edit-visibility"
|
||||||
|
name="visibility"
|
||||||
|
className="w-full h-10 px-3 py-2 border border-input rounded-md bg-background"
|
||||||
|
defaultValue="default"
|
||||||
|
>
|
||||||
|
<option value="default">Par défaut</option>
|
||||||
|
<option value="public">Public</option>
|
||||||
|
<option value="private">Privé</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-colorId">Couleur</Label>
|
||||||
|
<select
|
||||||
|
id="edit-colorId"
|
||||||
|
name="colorId"
|
||||||
|
className="w-full h-10 px-3 py-2 border border-input rounded-md bg-background"
|
||||||
|
defaultValue=""
|
||||||
|
>
|
||||||
|
<option value="">Par défaut</option>
|
||||||
|
<option value="1">Lavande</option>
|
||||||
|
<option value="2">Sauge</option>
|
||||||
|
<option value="3">Raisin</option>
|
||||||
|
<option value="4">Flamant rose</option>
|
||||||
|
<option value="5">Banane</option>
|
||||||
|
<option value="6">Mandarine</option>
|
||||||
|
<option value="7">Paon</option>
|
||||||
|
<option value="8">Graphite</option>
|
||||||
|
<option value="9">Myrtille</option>
|
||||||
|
<option value="10">Basilic</option>
|
||||||
|
<option value="11">Tomate</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2 pt-4">
|
||||||
|
<Button type="button" variant="outline" onClick={() => setEditEventModal({open: false, event: undefined})}>
|
||||||
|
Annuler
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">
|
||||||
|
Enregistrer les modifications
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* AI Chatbox */}
|
{/* AI Chatbox */}
|
||||||
{!chatOpen ? (
|
{!chatOpen ? (
|
||||||
<Button
|
<Button
|
||||||
@@ -2320,60 +2375,41 @@ const Index = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 space-y-2">
|
<div className="mt-3 flex gap-2">
|
||||||
{/* Search field */}
|
<select
|
||||||
<Input
|
value={selectedModel}
|
||||||
placeholder="Rechercher un modèle..."
|
onChange={(e) => setSelectedModel(e.target.value)}
|
||||||
value={modelSearchQuery}
|
className="flex-1 text-xs px-2 py-1.5 border border-input rounded-md bg-background"
|
||||||
onChange={(e) => setModelSearchQuery(e.target.value)}
|
disabled={chatLoading || loadingModels}
|
||||||
className="text-xs h-8"
|
>
|
||||||
disabled={loadingModels}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Models list */}
|
|
||||||
<div className="max-h-32 overflow-y-auto border rounded-md">
|
|
||||||
{loadingModels ? (
|
{loadingModels ? (
|
||||||
<div className="text-xs text-center py-2 text-muted-foreground">
|
<option value="">Chargement des modèles...</option>
|
||||||
Chargement des modèles...
|
|
||||||
</div>
|
|
||||||
) : openRouterModels.length === 0 ? (
|
) : openRouterModels.length === 0 ? (
|
||||||
<div className="text-xs text-center py-2 text-muted-foreground">
|
<option value="">Aucun modèle disponible</option>
|
||||||
Aucun modèle disponible
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
openRouterModels
|
openRouterModels.map(model => (
|
||||||
.filter(model =>
|
<option key={model.id} value={model.id}>
|
||||||
model.name.toLowerCase().includes(modelSearchQuery.toLowerCase()) ||
|
{model.name}
|
||||||
model.id.toLowerCase().includes(modelSearchQuery.toLowerCase())
|
</option>
|
||||||
)
|
))
|
||||||
.map(model => (
|
|
||||||
<div
|
|
||||||
key={model.id}
|
|
||||||
className={`flex items-center justify-between px-2 py-1.5 hover:bg-accent cursor-pointer text-xs ${
|
|
||||||
selectedModel === model.id ? 'bg-accent' : ''
|
|
||||||
}`}
|
|
||||||
onClick={() => setSelectedModel(model.id)}
|
|
||||||
>
|
|
||||||
<span className="flex-1 truncate">{model.name}</span>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-5 w-5"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleSetDefaultModel(model.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{defaultModel === model.id ? (
|
|
||||||
<span className="text-yellow-500">★</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-muted-foreground">☆</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</select>
|
||||||
|
{selectedModel && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8"
|
||||||
|
onClick={() => handleSetDefaultModel(selectedModel)}
|
||||||
|
disabled={loadingModels}
|
||||||
|
title="Définir comme modèle par défaut"
|
||||||
|
>
|
||||||
|
{defaultModel === selectedModel ? (
|
||||||
|
<span className="text-yellow-500">★</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground">☆</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface CalendarEvent {
|
|||||||
location?: string;
|
location?: string;
|
||||||
html_link?: string;
|
html_link?: string;
|
||||||
synced_at?: string;
|
synced_at?: string;
|
||||||
|
all_day?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CalendarService {
|
class CalendarService {
|
||||||
@@ -110,6 +111,25 @@ class CalendarService {
|
|||||||
const response = await axios.post(`${this.baseURL}/events`, eventData, this.getAuthHeader());
|
const response = await axios.post(`${this.baseURL}/events`, eventData, this.getAuthHeader());
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mettre à jour un événement dans Google Calendar
|
||||||
|
*/
|
||||||
|
async updateEvent(eventId: number, eventData: {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
startDateTime: string;
|
||||||
|
endDateTime: string;
|
||||||
|
location?: string;
|
||||||
|
attendees?: string[];
|
||||||
|
reminders?: Array<{ method: string; minutes: number }>;
|
||||||
|
recurrence?: string[];
|
||||||
|
visibility?: string;
|
||||||
|
colorId?: string;
|
||||||
|
}): Promise<{ eventId: string; htmlLink: string }> {
|
||||||
|
const response = await axios.put(`${this.baseURL}/events/${eventId}`, eventData, this.getAuthHeader());
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new CalendarService();
|
export default new CalendarService();
|
||||||
Reference in new issue
Block a user