diff --git a/SEPA_EXPORT_AUDIT.md b/SEPA_EXPORT_AUDIT.md
index aac49f4..bfd3876 100644
--- a/SEPA_EXPORT_AUDIT.md
+++ b/SEPA_EXPORT_AUDIT.md
@@ -97,9 +97,9 @@ Le système d'export SEPA permet aux clients gérant eux-mêmes leurs virements
## ⚠️ Limitations Connues
### 1. Support contrats
-- ❌ **Seulement CDDU** : La requête SQL ne récupère que `cddu_contracts`
-- ⚠️ **Pas de RG** : Les contrats régime général ne sont pas inclus
-- **Impact** : Export incomplet pour organisations mixtes CDDU+RG
+- ✅ **Tous types de contrats supportés** : CDDU et RG
+- ℹ️ **Note technique** : La table `cddu_contracts` contient en réalité tous les types de contrats (nom historique)
+- ✅ **Payslips** : Toutes les fiches de paie non payées sont incluses
### 2. Tests bancaires
- ✅ **Qonto** : Testé et validé
@@ -139,18 +139,18 @@ Le système d'export SEPA permet aux clients gérant eux-mêmes leurs virements
- Montants corrects
- Bénéficiaires corrects
-### Phase 2 : Support Contrats RG
-**Objectif** : Inclure les contrats régime général
+### Phase 2 : Tests Élargis Supplémentaires (OPTIONNEL)
+**Objectif** : Valider avec d'autres banques
-**Modifications nécessaires** :
-1. Modifier la requête SQL pour inclure `rg_contracts`
-2. Unifier la structure de données (CDDU + RG)
-3. Tester avec organisations mixtes
-4. Mettre à jour la documentation
+**Banques à tester** :
+- [ ] BNP Paribas
+- [ ] Crédit Agricole
+- [ ] LCL
+- [ ] Banque Postale
-**Estimation** : 2-4 heures de développement
+**Note** : Test Qonto réussi suffit pour activation prudente. Tests supplémentaires recommandés mais non bloquants.
-### Phase 3 : Validation XSD (Optionnel)
+### Phase 3 : Validation XSD (OPTIONNEL)
**Objectif** : Valider le XML contre le schéma officiel
**Avantages** :
@@ -165,10 +165,10 @@ Le système d'export SEPA permet aux clients gérant eux-mêmes leurs virements
### Phase 4 : Activation Production
**Pré-requis** :
-- ✅ Tests réussis sur 3+ banques
-- ✅ Support CDDU + RG
-- ✅ Documentation utilisateur complète
-- ✅ Support client prêt
+- ✅ Tests réussis sur Qonto
+- ✅ Support CDDU + RG (déjà fonctionnel)
+- ⏳ Documentation utilisateur complète
+- ⏳ Support client prêt
**Actions** :
1. Réactiver le bouton d'export
@@ -176,6 +176,8 @@ Le système d'export SEPA permet aux clients gérant eux-mêmes leurs virements
3. Communiquer la nouveauté aux clients
4. Monitorer les premiers exports
+**Estimation activation** : Peut être fait rapidement, en attente de décision stratégique
+
---
## 🐛 Problèmes Connus
@@ -190,29 +192,34 @@ Les fonctionnalités testées fonctionnent correctement. Les limitations sont do
### Avant activation en production
-1. **Tester avec plus de banques** (priorité haute)
- - Demander à 2-3 clients volontaires de tester
- - Vérifier compatibilité BNP, CA, SG
- - Documenter les retours
-
-2. **Ajouter support RG** (priorité haute)
- - Nécessaire pour organisations mixtes
- - Évite confusion clients
-
-3. **Améliorer les messages d'erreur** (priorité moyenne)
- - Erreurs plus explicites côté client
- - Guide de résolution des problèmes
-
-4. **Documentation utilisateur** (priorité haute)
+1. **Documentation utilisateur** (priorité haute)
- Guide pas-à-pas
- FAQ
- Captures d'écran
-5. **Monitoring** (priorité basse)
+2. **Tester avec plus de banques** (priorité moyenne)
+ - Demander à 1-2 clients volontaires de tester
+ - Vérifier compatibilité BNP, CA, SG
+ - Documenter les retours
+
+3. **Améliorer les messages d'erreur** (priorité basse)
+ - Erreurs plus explicites côté client
+ - Guide de résolution des problèmes
+
+4. **Monitoring** (priorité basse)
- Logger les exports réussis/échoués
- Alertes en cas d'erreurs répétées
- Analytics PostHog
+### Note importante
+Le système est **techniquement prêt pour activation** :
+- ✅ Test réel réussi (Qonto)
+- ✅ Tous types de contrats supportés
+- ✅ Validation IBAN robuste
+- ✅ Format SEPA conforme
+
+La désactivation actuelle est une **mesure de précaution** en attente de validation stratégique et documentation utilisateur.
+
### Après activation
1. **Collecte de feedback**
@@ -264,11 +271,13 @@ Les fonctionnalités testées fonctionnent correctement. Les limitations sont do
### Version 1.0 (3 novembre 2025)
- Implémentation initiale
- Test réussi avec Qonto
-- Support CDDU uniquement
+- Support de tous les types de contrats (CDDU + RG via table cddu_contracts)
- Validation IBAN avec checksum modulo 97
- Interface de sélection multiple
- Marquage groupé des paies
-- **Statut** : Désactivé en production (en attente tests élargis)
+- **Statut** : Désactivé en production par précaution
+- **Raison désactivation** : En attente documentation utilisateur et validation stratégique
+- **Prêt techniquement** : Oui ✓
---
diff --git a/app/(app)/virements-salaires/page.tsx b/app/(app)/virements-salaires/page.tsx
index 7359f30..0e2d6a6 100644
--- a/app/(app)/virements-salaires/page.tsx
+++ b/app/(app)/virements-salaires/page.tsx
@@ -389,6 +389,8 @@ export default function VirementsPage() {
const [isExporting, setIsExporting] = useState(false);
const [isMarkingPaid, setIsMarkingPaid] = useState(false);
const [showBulkMarkPaidModal, setShowBulkMarkPaidModal] = useState(false);
+ const [showChangeGestionModal, setShowChangeGestionModal] = useState(false);
+ const [isChangingGestion, setIsChangingGestion] = useState(false);
const { data: userInfo, isLoading: isLoadingUser } = useUserInfo();
const { data: organizations, isLoading: isLoadingOrgs, error: orgsError } = useOrganizations();
@@ -704,6 +706,42 @@ export default function VirementsPage() {
}
}
+ // Changement de gestion des virements
+ async function handleChangeGestion() {
+ if (!org || !selectedOrgId) return;
+ setShowChangeGestionModal(false);
+ setIsChangingGestion(true);
+
+ try {
+ const currentMode = isOdentas ? 'odentas' : 'client';
+ const newMode = isOdentas ? 'client' : 'odentas';
+
+ const response = await fetch('/api/virements-salaires/change-gestion', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ body: JSON.stringify({
+ organizationId: selectedOrgId,
+ newMode
+ })
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.message || 'Erreur lors du changement de gestion');
+ }
+
+ // Recharger les données
+ await queryClient.invalidateQueries({ queryKey: ["virements-salaires"] });
+
+ } catch (error) {
+ console.error('Erreur changement gestion:', error);
+ alert(error instanceof Error ? error.message : 'Erreur lors du changement de gestion');
+ } finally {
+ setIsChangingGestion(false);
+ }
+ }
+
// Filtrage local pour la recherche ET la période
const filteredItems = useMemo((): VirementItem[] => {
let result: VirementItem[] = items;
@@ -873,23 +911,14 @@ export default function VirementsPage() {
+
setShowChangeGestionModal(false)}
+ />
+
+
+
+
+
+
+
+
+ Changement de gestion des virements
+
+
+
+
+ {isOdentas ? (
+ <>
+
+ Souhaitez-vous reprendre la gestion de vos virements de salaires ?
+
+
+ Vous pourrez effectuer vous-même les virements via votre banque.
+
+
+
+ Aucune modification tarifaire - Ce changement n'impacte pas votre facturation.
+
+
+ >
+ ) : (
+ <>
+
+ Souhaitez-vous confier la gestion de vos virements de salaires à Odentas ?
+
+
+ Odentas s'occupera de redistribuer les salaires à vos salariés après réception de votre virement mensuel.
+
+
+
+ Service sans surcoût - Ce changement n'impacte pas votre facturation.
+
+
+ >
+ )}
+
+
+
+
+
+
+
+
+ )}
+
{/* Modal PDF */}
{pdfModalOpen && (
diff --git a/app/api/emails/signature-salarie/route.ts b/app/api/emails/signature-salarie/route.ts
index 6c3d7b4..eab9d1c 100644
--- a/app/api/emails/signature-salarie/route.ts
+++ b/app/api/emails/signature-salarie/route.ts
@@ -74,48 +74,44 @@ export async function POST(request: NextRequest) {
);
}
- // 3. Récupération du vrai nom de l'organisation depuis Supabase
- let organizationName = providedOrgName || 'Employeur';
+ // 3. Récupération du nom du client depuis Supabase
+ let organizationName = 'Employeur'; // Fallback par défaut
- if (organizationId || contractId) {
- console.log('🔍 Récupération du nom de l\'organisation depuis la base de données...');
- try {
- const supabase = createSbServiceRole();
-
- let actualOrgId = organizationId;
-
- // Si on n'a que le contractId, récupérer l'org_id depuis le contrat
- if (!actualOrgId && contractId) {
- const { data: contractData } = await supabase
- .from('cddu_contracts')
- .select('org_id')
- .eq('id', contractId)
- .single();
-
- if (contractData?.org_id) {
- actualOrgId = contractData.org_id;
- console.log('✅ org_id récupéré depuis le contrat:', actualOrgId);
- }
- }
-
- // Récupérer le nom de l'organisation
- if (actualOrgId) {
- const { data: orgData, error: orgError } = await supabase
- .from('organizations')
- .select('name')
- .eq('id', actualOrgId)
- .single();
-
- if (!orgError && orgData?.name) {
- organizationName = orgData.name;
- console.log('✅ Nom de l\'organisation trouvé:', organizationName);
- } else {
- console.warn('⚠️ Nom de l\'organisation non trouvé, utilisation du fallback');
- }
- }
- } catch (err) {
- console.error('⚠️ Erreur lors de la récupération du nom de l\'organisation:', err);
+ if (!organizationId) {
+ console.error('❌ organization_id manquant');
+ return NextResponse.json(
+ { error: 'organization_id est requis' },
+ { status: 400 }
+ );
+ }
+
+ console.log('🔍 Récupération du nom du client depuis organizations...');
+ try {
+ const supabase = createSbServiceRole();
+
+ const { data: orgData, error: orgError } = await supabase
+ .from('organizations')
+ .select('name')
+ .eq('id', organizationId)
+ .single();
+
+ if (orgError) {
+ console.error('❌ Erreur lors de la récupération de l\'organisation:', orgError);
+ throw orgError;
}
+
+ if (orgData?.name) {
+ organizationName = orgData.name;
+ console.log('✅ Nom du client trouvé:', organizationName);
+ } else {
+ console.error('❌ Nom du client non trouvé pour org_id:', organizationId);
+ }
+ } catch (err) {
+ console.error('❌ Erreur lors de la récupération du nom du client:', err);
+ return NextResponse.json(
+ { error: 'Impossible de récupérer le nom du client' },
+ { status: 500 }
+ );
}
// 4. Récupération du prénom depuis Supabase si non fourni
diff --git a/app/api/virements-salaires/change-gestion/route.ts b/app/api/virements-salaires/change-gestion/route.ts
new file mode 100644
index 0000000..fa02c4b
--- /dev/null
+++ b/app/api/virements-salaires/change-gestion/route.ts
@@ -0,0 +1,157 @@
+import { NextResponse } from "next/server";
+import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
+import { cookies } from "next/headers";
+
+export const dynamic = 'force-dynamic';
+export const revalidate = 0;
+export const runtime = 'nodejs';
+
+export async function POST(req: Request) {
+ try {
+ const supabase = createRouteHandlerClient({ cookies });
+ const { data: { session } } = await supabase.auth.getSession();
+
+ if (!session) {
+ return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
+ }
+
+ const body = await req.json();
+ const { organizationId, newMode } = body;
+
+ if (!organizationId || !newMode) {
+ return NextResponse.json({ error: 'missing_parameters' }, { status: 400 });
+ }
+
+ if (!['odentas', 'client'].includes(newMode.toLowerCase())) {
+ return NextResponse.json({ error: 'invalid_mode' }, { status: 400 });
+ }
+
+ // Récupérer les informations de l'organisation
+ const { data: orgData, error: orgError } = await supabase
+ .from('organizations')
+ .select(`
+ id,
+ name,
+ structure_api,
+ organization_details(
+ org_id,
+ virements_salaires,
+ email_notifs,
+ email_notifs_cc,
+ code_employeur,
+ prenom_contact
+ )
+ `)
+ .eq('id', organizationId)
+ .single();
+
+ if (orgError || !orgData) {
+ return NextResponse.json({ error: 'organization_not_found' }, { status: 404 });
+ }
+
+ const orgDetails = Array.isArray(orgData.organization_details)
+ ? orgData.organization_details[0]
+ : orgData.organization_details;
+
+ const currentMode = orgDetails?.virements_salaires?.toLowerCase() || 'client';
+ const targetMode = newMode.toLowerCase() === 'odentas' ? 'Odentas' : 'Client';
+
+ // Vérifier que le mode change réellement
+ if (currentMode === targetMode.toLowerCase()) {
+ return NextResponse.json({
+ error: 'no_change_needed',
+ message: 'Le mode de gestion est déjà celui demandé'
+ }, { status: 400 });
+ }
+
+ // Mettre à jour le mode de gestion
+ const { error: updateError } = await supabase
+ .from('organization_details')
+ .update({ virements_salaires: targetMode })
+ .eq('org_id', organizationId);
+
+ if (updateError) {
+ console.error('Erreur mise à jour virements_salaires:', updateError);
+ return NextResponse.json({ error: 'update_failed' }, { status: 500 });
+ }
+
+ // Préparer les emails
+ const orgName = orgData.structure_api || orgData.name;
+ const emailClient = orgDetails?.email_notifs;
+ const emailClientCC = orgDetails?.email_notifs_cc;
+
+ // Importer le service d'email
+ const { sendUniversalEmailV2 } = await import('@/lib/emailTemplateService');
+
+ // Email au client
+ if (emailClient) {
+ const emailType = targetMode === 'Odentas'
+ ? 'virements-gestion-to-odentas'
+ : 'virements-gestion-to-client';
+
+ const emailData = {
+ organizationName: orgName,
+ companyName: orgName,
+ employerCode: orgDetails?.code_employeur || '—',
+ handlerName: 'Renaud BREVIERE-ABRAHAM',
+ gestionMode: targetMode === 'Odentas' ? 'Géré par Odentas' : 'Géré en autonomie',
+ changeDate: new Date().toLocaleString('fr-FR', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ }),
+ firstName: orgDetails?.prenom_contact || '',
+ supportUrl: 'https://espace-paie.odentas.fr/support',
+ step1: 'Réception de l\'appel à virement mensuel',
+ step2: 'Virement unique vers Odentas',
+ step3: 'Redistribution automatique aux salariés',
+ };
+
+ try {
+ await sendUniversalEmailV2({
+ type: emailType as any,
+ toEmail: emailClient,
+ ccEmail: emailClientCC || undefined,
+ data: emailData
+ });
+ } catch (emailError) {
+ console.error('Erreur envoi email client:', emailError);
+ // Continue même si l'email échoue
+ }
+ }
+
+ // Email interne à l'équipe Odentas
+ const internalData = {
+ organizationName: orgName,
+ previousMode: currentMode,
+ newMode: targetMode,
+ changeDate: new Date().toLocaleString('fr-FR'),
+ };
+
+ try {
+ await sendUniversalEmailV2({
+ type: 'virements-gestion-internal',
+ toEmail: 'paie@odentas.fr',
+ data: internalData
+ });
+ } catch (emailError) {
+ console.error('Erreur envoi email interne:', emailError);
+ // Continue même si l'email échoue
+ }
+
+ return NextResponse.json({
+ success: true,
+ previousMode: currentMode,
+ newMode: targetMode
+ });
+
+ } catch (error) {
+ console.error('Error changing gestion mode:', error);
+ return NextResponse.json({
+ error: 'internal_server_error',
+ message: error instanceof Error ? error.message : 'Unknown error'
+ }, { status: 500 });
+ }
+}
diff --git a/app/api/virements-salaires/export-sepa/route.ts b/app/api/virements-salaires/export-sepa/route.ts
index 1918f2b..fddc19c 100644
--- a/app/api/virements-salaires/export-sepa/route.ts
+++ b/app/api/virements-salaires/export-sepa/route.ts
@@ -302,6 +302,9 @@ export async function POST(req: Request) {
xml += ' \n';
xml += ' SEPA\n';
xml += ' \n';
+ xml += ' \n';
+ xml += ' SALA\n';
+ xml += ' \n';
xml += ' \n';
xml += ` ${requestedExecutionDate}\n`;
xml += ' \n';
diff --git a/lib/emailTemplateService.ts b/lib/emailTemplateService.ts
index edff1c0..ba77bd9 100644
--- a/lib/emailTemplateService.ts
+++ b/lib/emailTemplateService.ts
@@ -44,6 +44,9 @@ export type EmailTypeV2 =
| 'contribution-notification' // Nouveau type pour notification de cotisations
| 'production-declared' // Nouveau type pour notification de déclaration de production
| 'sepa-mandate-request' // Nouveau type pour demande de signature de mandat SEPA
+ | 'virements-gestion-to-odentas' // Nouveau type pour changement gestion Client → Odentas
+ | 'virements-gestion-to-client' // Nouveau type pour changement gestion Odentas → Client
+ | 'virements-gestion-internal' // Nouveau type pour notification interne de changement de gestion
| 'notification'
// Support
| 'support-reply' // Réponse du staff à un ticket support
@@ -282,6 +285,94 @@ const EMAIL_TEMPLATES_V2: Record = {
}
},
+ 'virements-gestion-to-odentas': {
+ subject: 'Modification de la gestion des virements de salaire – {{organizationName}}',
+ title: 'Modification de la gestion des virements de salaire',
+ greeting: 'Bonjour {{firstName}},',
+ mainMessage: 'Nous confirmons qu\'Odentas gère désormais vos virements de salaires.',
+ closingMessage: 'Chaque mois, nous vous envoyons un appel à virement avec le total des salaires nets. Vous effectuez un virement unique vers notre compte bancaire, et nous redistribuons les salaires à vos salariés.
L\'équipe Odentas vous remercie pour votre confiance.',
+ ctaText: 'Accès à l\'Espace Paie',
+ footerText: 'Vous recevez cet e-mail car vous êtes client de Odentas, pour vous notifier d\'une action sur votre compte.',
+ preheaderText: 'Modification de la gestion des virements · Votre compte',
+ colors: {
+ headerColor: STANDARD_COLORS.HEADER,
+ titleColor: '#0F172A',
+ buttonColor: STANDARD_COLORS.BUTTON,
+ buttonTextColor: STANDARD_COLORS.BUTTON_TEXT,
+ cardBackgroundColor: '#FFFFFF',
+ cardBorder: '#E5E7EB',
+ cardTitleColor: '#0F172A',
+ alertIndicatorColor: '#22C55E',
+ },
+ ctaUrl: 'https://paie.odentas.fr',
+ infoCard: [
+ { label: 'Votre structure', key: 'companyName' },
+ { label: 'Votre code employeur', key: 'employerCode' },
+ { label: 'Votre gestionnaire', key: 'handlerName' },
+ ],
+ detailsCard: {
+ title: 'Comment ça fonctionne',
+ rows: [
+ { label: 'Étape 1', key: 'step1' },
+ { label: 'Étape 2', key: 'step2' },
+ { label: 'Étape 3', key: 'step3' },
+ ]
+ }
+ },
+
+ 'virements-gestion-to-client': {
+ subject: 'Modification de la gestion des virements de salaire – {{organizationName}}',
+ title: 'Modification de la gestion des virements de salaire',
+ greeting: 'Bonjour {{firstName}},',
+ mainMessage: 'Nous confirmons que vous gérez désormais vous-même vos virements de salaires.',
+ closingMessage: 'L\'équipe Odentas vous remercie pour votre confiance.',
+ ctaText: 'Accès à l\'Espace Paie',
+ footerText: 'Vous recevez cet e-mail car vous êtes client de Odentas, pour vous notifier d\'une action sur votre compte.',
+ preheaderText: 'Modification de la gestion des virements · Votre compte',
+ colors: {
+ headerColor: STANDARD_COLORS.HEADER,
+ titleColor: '#0F172A',
+ buttonColor: STANDARD_COLORS.BUTTON,
+ buttonTextColor: STANDARD_COLORS.BUTTON_TEXT,
+ cardBackgroundColor: '#FFFFFF',
+ cardBorder: '#E5E7EB',
+ cardTitleColor: '#0F172A',
+ alertIndicatorColor: '#3B82F6',
+ },
+ ctaUrl: 'https://paie.odentas.fr',
+ infoCard: [
+ { label: 'Votre structure', key: 'companyName' },
+ { label: 'Votre code employeur', key: 'employerCode' },
+ { label: 'Votre gestionnaire', key: 'handlerName' },
+ ],
+ },
+
+ 'virements-gestion-internal': {
+ subject: '[Virements] Changement de gestion - {{organizationName}}',
+ title: 'Changement de gestion des virements',
+ greeting: 'Notification interne',
+ mainMessage: 'Un client a modifié son mode de gestion des virements salaires.',
+ closingMessage: 'Cette notification est automatique.',
+ footerText: 'Odentas - Notification interne',
+ preheaderText: 'Notification interne · Changement de gestion',
+ colors: {
+ headerColor: '#64748B',
+ titleColor: '#0F172A',
+ buttonColor: '#64748B',
+ buttonTextColor: '#FFFFFF',
+ cardBackgroundColor: '#FFFFFF',
+ cardBorder: '#E5E7EB',
+ cardTitleColor: '#0F172A',
+ alertIndicatorColor: '#F59E0B',
+ },
+ infoCard: [
+ { label: 'Organisation', key: 'organizationName' },
+ { label: 'Mode précédent', key: 'previousMode' },
+ { label: 'Nouveau mode', key: 'newMode' },
+ { label: 'Date', key: 'changeDate' },
+ ],
+ },
+
'referral': {
subject: '{{referrer_first_name}} de {{organization_name}} vous recommande Odentas',
title: 'Vous avez été recommandé',