import { supabase } from './supabase'; import { Campaign, Proposition, Participant, Vote, ParticipantWithVoteStatus, Setting, SmtpSettings } from '@/types'; import { encryptionService } from './encryption'; import { emailService } from './email'; // Fonction utilitaire pour générer un slug côté client function generateSlugClient(title: string): string { // Convertir en minuscules et remplacer les caractères spéciaux let slug = title.toLowerCase() .replace(/[^a-z0-9\s]/g, '') .replace(/\s+/g, '-') .trim(); // Si le slug est vide, utiliser 'campagne' if (!slug) { slug = 'campagne'; } // Ajouter un timestamp pour éviter les conflits const timestamp = Date.now().toString().slice(-6); return `${slug}-${timestamp}`; } // Fonction utilitaire pour générer un short_id côté client function generateShortIdClient(): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; let result = ''; // Générer un identifiant de 6 caractères for (let i = 0; i < 6; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } // Ajouter un timestamp pour éviter les conflits const timestamp = Date.now().toString().slice(-3); return `${result}${timestamp}`; } // Fonction utilitaire pour gérer les erreurs Supabase function handleSupabaseError(error: any, operation: string): never { console.error(`Erreur Supabase lors de ${operation}:`, error); // Extraire les détails de l'erreur let errorMessage = `Erreur lors de ${operation}`; if (error?.message) { errorMessage = error.message; } else if (error?.error_description) { errorMessage = error.error_description; } else if (error?.details) { errorMessage = error.details; } else if (typeof error === 'string') { errorMessage = error; } // Ajouter des informations de débogage const debugInfo = { operation, error, timestamp: new Date().toISOString(), userAgent: typeof window !== 'undefined' ? window.navigator.userAgent : 'server' }; console.error('Informations de débogage:', debugInfo); throw new Error(errorMessage); } // Services pour les campagnes export const campaignService = { async getAll(): Promise { const { data, error } = await supabase .from('campaigns') .select('*') .order('created_at', { ascending: false }); if (error) throw error; return data || []; }, // eslint-disable-next-line @typescript-eslint/no-explicit-any async create(campaign: any): Promise { // Générer automatiquement le slug si non fourni if (!campaign.slug) { try { // Essayer d'utiliser la fonction PostgreSQL const { data: slugData, error: slugError } = await supabase .rpc('generate_slug', { title: campaign.title }); if (slugError) { // Si la fonction n'existe pas, générer le slug côté client console.warn('Fonction generate_slug non disponible, génération côté client:', slugError); campaign.slug = generateSlugClient(campaign.title); } else { campaign.slug = slugData; } } catch (error) { // Fallback vers la génération côté client console.warn('Erreur avec generate_slug, génération côté client:', error); campaign.slug = generateSlugClient(campaign.title); } } const { data, error } = await supabase .from('campaigns') .insert(campaign) .select() .single(); if (error) throw error; return data; }, // eslint-disable-next-line @typescript-eslint/no-explicit-any async update(id: string, updates: any): Promise { // Générer automatiquement le slug si le titre a changé et qu'aucun slug n'est fourni if (updates.title && !updates.slug) { try { // Essayer d'utiliser la fonction PostgreSQL const { data: slugData, error: slugError } = await supabase .rpc('generate_slug', { title: updates.title }); if (slugError) { // Si la fonction n'existe pas, générer le slug côté client console.warn('Fonction generate_slug non disponible, génération côté client:', slugError); updates.slug = generateSlugClient(updates.title); } else { updates.slug = slugData; } } catch (error) { // Fallback vers la génération côté client console.warn('Erreur avec generate_slug, génération côté client:', error); updates.slug = generateSlugClient(updates.title); } } const { data, error } = await supabase .from('campaigns') .update(updates) .eq('id', id) .select() .single(); if (error) throw error; return data; }, async delete(id: string): Promise { console.log('Tentative de suppression de la campagne:', id); // La suppression en cascade est gérée par la base de données // grâce à ON DELETE CASCADE dans les contraintes de clés étrangères const { error } = await supabase .from('campaigns') .delete() .eq('id', id); if (error) { console.error('Erreur lors de la suppression:', error); throw error; } console.log('Campagne supprimée avec succès'); }, async getStats(campaignId: string): Promise<{ propositions: number; participants: number }> { const [propositionsResult, participantsResult] = await Promise.all([ supabase .from('propositions') .select('id', { count: 'exact', head: true }) .eq('campaign_id', campaignId), supabase .from('participants') .select('id', { count: 'exact', head: true }) .eq('campaign_id', campaignId) ]); if (propositionsResult.error) throw propositionsResult.error; if (participantsResult.error) throw participantsResult.error; return { propositions: propositionsResult.count || 0, participants: participantsResult.count || 0 }; }, // Nouvelle méthode pour récupérer une campagne par slug async getBySlug(slug: string): Promise { const { data, error } = await supabase .from('campaigns') .select('*') .eq('slug', slug) .single(); if (error) { if (error.code === 'PGRST116') { return null; // Aucune campagne trouvée } throw error; } return data; } }; // Services pour les propositions export const propositionService = { async getByCampaign(campaignId: string): Promise { const { data, error } = await supabase .from('propositions') .select('*') .eq('campaign_id', campaignId) .order('created_at', { ascending: false }); if (error) throw error; return data || []; }, // eslint-disable-next-line @typescript-eslint/no-explicit-any async create(proposition: any): Promise { const { data, error } = await supabase .from('propositions') .insert(proposition) .select() .single(); if (error) throw error; return data; }, // eslint-disable-next-line @typescript-eslint/no-explicit-any async update(id: string, updates: any): Promise { try { // Effectuer la mise à jour directement const { data, error } = await supabase .from('propositions') .update(updates) .eq('id', id) .select('*') .single(); if (error) { console.error('Erreur Supabase lors de la mise à jour:', error); if (error.code === 'PGRST116') { throw new Error(`Proposition avec l'ID ${id} non trouvée`); } throw new Error(`Erreur lors de la mise à jour: ${error.message || 'Erreur inconnue'}`); } if (!data) { throw new Error('Aucune donnée retournée après la mise à jour'); } return data; } catch (error: any) { console.error('Erreur dans propositionService.update:', error); throw error; } }, async delete(id: string): Promise { const { error } = await supabase .from('propositions') .delete() .eq('id', id); if (error) throw error; } }; // Services pour les participants export const participantService = { async getByCampaign(campaignId: string): Promise { const { data, error } = await supabase .from('participants') .select('*') .eq('campaign_id', campaignId) .order('created_at', { ascending: false }); if (error) throw error; return data || []; }, // eslint-disable-next-line @typescript-eslint/no-explicit-any async create(participant: any): Promise { // Générer automatiquement le short_id si non fourni if (!participant.short_id) { try { // Essayer d'utiliser la fonction PostgreSQL const { data: shortIdData, error: shortIdError } = await supabase .rpc('generate_short_id'); if (shortIdError) { // Si la fonction n'existe pas, générer le short_id côté client console.warn('Fonction generate_short_id non disponible, génération côté client:', shortIdError); participant.short_id = generateShortIdClient(); } else { participant.short_id = shortIdData; } } catch (error) { // Fallback vers la génération côté client console.warn('Erreur avec generate_short_id, génération côté client:', error); participant.short_id = generateShortIdClient(); } } const { data, error } = await supabase .from('participants') .insert(participant) .select() .single(); if (error) throw error; return data; }, // eslint-disable-next-line @typescript-eslint/no-explicit-any async update(id: string, updates: any): Promise { try { // Effectuer la mise à jour directement const { data, error } = await supabase .from('participants') .update(updates) .eq('id', id) .select('*') .single(); if (error) { console.error('Erreur Supabase lors de la mise à jour du participant:', error); if (error.code === 'PGRST116') { throw new Error(`Participant avec l'ID ${id} non trouvé`); } throw new Error(`Erreur lors de la mise à jour du participant: ${error.message || 'Erreur inconnue'}`); } if (!data) { throw new Error('Aucune donnée retournée après la mise à jour du participant'); } return data; } catch (error: any) { console.error('Erreur dans participantService.update:', error); throw error; } }, async delete(id: string): Promise { const { error } = await supabase .from('participants') .delete() .eq('id', id); if (error) throw error; }, // Nouvelle méthode pour récupérer un participant par short_id async getByShortId(shortId: string): Promise { const { data, error } = await supabase .from('participants') .select('*') .eq('short_id', shortId) .single(); if (error) { if (error.code === 'PGRST116') { return null; // Aucun participant trouvé } throw error; } return data; } }; // Services pour les votes export const voteService = { async getByParticipant(campaignId: string, participantId: string): Promise { const { data, error } = await supabase .from('votes') .select('*') .eq('campaign_id', campaignId) .eq('participant_id', participantId); if (error) handleSupabaseError(error, 'récupération des votes par participant'); return data || []; }, async getByProposition(propositionId: string): Promise { const { data, error } = await supabase .from('votes') .select('*') .eq('proposition_id', propositionId); if (error) handleSupabaseError(error, 'récupération des votes par proposition'); return data || []; }, // eslint-disable-next-line @typescript-eslint/no-explicit-any async create(vote: any): Promise { const { data, error } = await supabase .from('votes') .insert(vote) .select() .single(); if (error) handleSupabaseError(error, 'création de vote'); return data; }, // eslint-disable-next-line @typescript-eslint/no-explicit-any async update(id: string, updates: any): Promise { const { data, error } = await supabase .from('votes') .update(updates) .eq('id', id) .select() .single(); if (error) handleSupabaseError(error, 'mise à jour de vote'); return data; }, async upsert(vote: { campaign_id: string; participant_id: string; proposition_id: string; amount: number }): Promise { const { data, error } = await supabase .from('votes') .upsert(vote, { onConflict: 'participant_id,proposition_id' }) .select() .single(); if (error) handleSupabaseError(error, 'upsert de vote'); return data; }, async delete(id: string): Promise { const { error } = await supabase .from('votes') .delete() .eq('id', id); if (error) handleSupabaseError(error, 'suppression de vote'); }, async getByCampaign(campaignId: string): Promise { const { data, error } = await supabase .from('votes') .select('*') .eq('campaign_id', campaignId); if (error) handleSupabaseError(error, 'récupération des votes par campagne'); return data || []; }, async getParticipantVoteStatus(campaignId: string): Promise { const { data: participants, error: participantsError } = await supabase .from('participants') .select('*') .eq('campaign_id', campaignId); if (participantsError) throw participantsError; const { data: votes, error: votesError } = await supabase .from('votes') .select('*') .eq('campaign_id', campaignId); if (votesError) throw votesError; return participants.map(participant => { const participantVotes = votes.filter(vote => vote.participant_id === participant.id); const totalVotedAmount = participantVotes.reduce((sum, vote) => sum + vote.amount, 0); return { ...participant, has_voted: participantVotes.length > 0, total_voted_amount: totalVotedAmount }; }); }, // Méthode pour remplacer tous les votes d'un participant de manière atomique async replaceVotes( campaignId: string, participantId: string, votes: Array<{ proposition_id: string; amount: number }> ): Promise { // Utiliser une transaction pour garantir l'atomicité const { error } = await supabase.rpc('replace_participant_votes', { p_campaign_id: campaignId, p_participant_id: participantId, p_votes: votes }); if (error) handleSupabaseError(error, 'remplacement des votes du participant'); } }; // Services pour les paramètres export const settingsService = { async getAll(): Promise { const { data, error } = await supabase .from('settings') .select('*') .order('category', { ascending: true }) .order('key', { ascending: true }); if (error) throw error; return data || []; }, async getByCategory(category: string): Promise { const { data, error } = await supabase .from('settings') .select('*') .eq('category', category) .order('key', { ascending: true }); if (error) throw error; return data || []; }, async getByKey(key: string): Promise { const { data, error } = await supabase .from('settings') .select('*') .eq('key', key) .single(); if (error) { if (error.code === 'PGRST116') return null; // No rows returned throw error; } return data; }, async getValue(key: string, defaultValue: string = ''): Promise { const setting = await this.getByKey(key); return setting?.value || defaultValue; }, async getBooleanValue(key: string, defaultValue: boolean = false): Promise { const value = await this.getValue(key, defaultValue.toString()); return value === 'true'; }, // eslint-disable-next-line @typescript-eslint/no-explicit-any async create(setting: any): Promise { const { data, error } = await supabase .from('settings') .insert(setting) .select() .single(); if (error) throw error; return data; }, // eslint-disable-next-line @typescript-eslint/no-explicit-any async update(key: string, updates: any): Promise { const { data, error } = await supabase .from('settings') .update(updates) .eq('key', key) .select() .single(); if (error) throw error; return data; }, async setValue(key: string, value: string): Promise { const existing = await this.getByKey(key); if (existing) { return this.update(key, { value }); } else { return this.create({ key, value, category: 'general' }); } }, async setBooleanValue(key: string, value: boolean): Promise { return this.setValue(key, value.toString()); }, async delete(key: string): Promise { const { error } = await supabase .from('settings') .delete() .eq('key', key); if (error) throw error; }, // Méthodes spécifiques pour les paramètres SMTP async getSmtpSettings(): Promise { const smtpKeys = [ 'smtp_host', 'smtp_port', 'smtp_username', 'smtp_password', 'smtp_secure', 'smtp_from_email', 'smtp_from_name' ]; const settings = await Promise.all( smtpKeys.map(key => this.getByKey(key)) ); return { host: settings[0]?.value || '', port: parseInt(settings[1]?.value || '587'), username: settings[2]?.value || '', password: encryptionService.isEncrypted(settings[3]?.value || '') ? encryptionService.decrypt(settings[3]?.value || '') : settings[3]?.value || '', secure: settings[4]?.value === 'true', from_email: settings[5]?.value || '', from_name: settings[6]?.value || '' }; }, async setSmtpSettings(smtpSettings: SmtpSettings): Promise { const settingsToUpdate = [ { key: 'smtp_host', value: smtpSettings.host, category: 'email', description: 'Serveur SMTP' }, { key: 'smtp_port', value: smtpSettings.port.toString(), category: 'email', description: 'Port SMTP' }, { key: 'smtp_username', value: smtpSettings.username, category: 'email', description: 'Nom d\'utilisateur SMTP' }, { key: 'smtp_password', value: encryptionService.encrypt(smtpSettings.password), category: 'email', description: 'Mot de passe SMTP (chiffré)' }, { key: 'smtp_secure', value: smtpSettings.secure.toString(), category: 'email', description: 'Connexion sécurisée SSL/TLS' }, { key: 'smtp_from_email', value: smtpSettings.from_email, category: 'email', description: 'Adresse email d\'expédition' }, { key: 'smtp_from_name', value: smtpSettings.from_name, category: 'email', description: 'Nom d\'expédition' } ]; await Promise.all( settingsToUpdate.map(setting => this.setValue(setting.key, setting.value)) ); }, async testSmtpConnection(smtpSettings: SmtpSettings): Promise<{ success: boolean; error?: string }> { try { // Validation basique des paramètres if (!smtpSettings.host || !smtpSettings.port || !smtpSettings.username || !smtpSettings.password) { return { success: false, error: 'Paramètres SMTP incomplets' }; } if (smtpSettings.port < 1 || smtpSettings.port > 65535) { return { success: false, error: 'Port SMTP invalide' }; } if (!smtpSettings.from_email.includes('@')) { return { success: false, error: 'Adresse email d\'expédition invalide' }; } // Test de connexion via API route return await emailService.testConnection(smtpSettings); } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Erreur inconnue' }; } }, async sendTestEmail(smtpSettings: SmtpSettings, toEmail: string): Promise<{ success: boolean; error?: string; messageId?: string }> { try { // Validation de l'email de destination if (!emailService.validateEmail(toEmail)) { return { success: false, error: 'Adresse email de destination invalide' }; } // Envoi de l'email de test via API route return await emailService.sendTestEmail(smtpSettings, toEmail); } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Erreur lors de l\'envoi de l\'email de test' }; } } };