fix: Récupérer le nom du client depuis Supabase pour email signature-request-salarie

This commit is contained in:
odentas 2025-11-03 15:23:25 +01:00
parent 265ed6ce67
commit 5502926271
6 changed files with 445 additions and 90 deletions

View file

@ -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 ✓
---

View file

@ -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() {
<div className="text-xs text-slate-600">Les virements de salaires sont effectués par</div>
</div>
<div className="flex items-center gap-2">
<div className="relative group inline-block">
<button
type="button"
disabled
aria-disabled="true"
className="text-xs px-2 py-1 rounded-md border opacity-60 cursor-not-allowed"
>
Modifier
</button>
<div
role="tooltip"
className="pointer-events-none absolute right-0 mt-2 w-64 px-3 py-2 rounded-lg bg-slate-900 text-white text-xs shadow-lg opacity-0 group-hover:opacity-100 translate-y-1 group-hover:translate-y-0 transition"
>
Bientôt disponible, veuillez nous contacter.
<div className="absolute -top-1 right-6 w-2 h-2 rotate-45 bg-slate-900" />
</div>
</div>
<button
type="button"
onClick={() => setShowChangeGestionModal(true)}
disabled={loadingOrg || isChangingGestion}
className="text-xs px-2 py-1 rounded-md border hover:bg-slate-50 transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
>
{isChangingGestion ? 'Modification...' : 'Modifier'}
</button>
</div>
</div>
<div className="mt-3 flex items-center justify-between gap-2">
@ -1352,6 +1381,76 @@ export default function VirementsPage() {
</div>
)}
{/* Modal de changement de gestion des virements */}
{showChangeGestionModal && (
<div className="fixed inset-0 z-[1000]">
<div
className="absolute inset-0 bg-black/40"
onClick={() => setShowChangeGestionModal(false)}
/>
<div className="absolute inset-0 flex items-center justify-center p-4">
<div role="dialog" aria-modal="true" className="w-full max-w-md rounded-2xl border bg-white shadow-xl">
<div className="p-5 border-b bg-blue-50">
<div className="flex items-center gap-3">
<div className="rounded-lg bg-blue-100 p-2">
<Info className="h-5 w-5 text-blue-700" />
</div>
<h2 className="text-base font-semibold text-blue-900">
Changement de gestion des virements
</h2>
</div>
</div>
<div className="p-5 space-y-4 text-sm">
{isOdentas ? (
<>
<p className="text-slate-700">
Souhaitez-vous reprendre la <strong>gestion de vos virements de salaires</strong> ?
</p>
<p className="text-slate-600">
Vous pourrez effectuer vous-même les virements via votre banque.
</p>
<div className="rounded-lg bg-green-50 border border-green-200 p-3">
<p className="text-sm text-green-900">
<strong>Aucune modification tarifaire</strong> - Ce changement n'impacte pas votre facturation.
</p>
</div>
</>
) : (
<>
<p className="text-slate-700">
Souhaitez-vous <strong>confier la gestion de vos virements de salaires à Odentas</strong> ?
</p>
<p className="text-slate-600">
Odentas s'occupera de redistribuer les salaires à vos salariés après réception de votre virement mensuel.
</p>
<div className="rounded-lg bg-green-50 border border-green-200 p-3">
<p className="text-sm text-green-900">
<strong>Service sans surcoût</strong> - Ce changement n'impacte pas votre facturation.
</p>
</div>
</>
)}
</div>
<div className="p-4 border-t flex justify-end gap-2">
<button
onClick={() => setShowChangeGestionModal(false)}
className="px-4 py-2 rounded-lg border hover:bg-slate-50 text-sm transition-colors"
>
Annuler
</button>
<button
onClick={handleChangeGestion}
disabled={isChangingGestion}
className="px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700 text-sm transition-colors disabled:opacity-50"
>
{isChangingGestion ? 'Modification en cours...' : 'Confirmer'}
</button>
</div>
</div>
</div>
</div>
)}
{/* Modal PDF */}
{pdfModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">

View file

@ -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();
if (!organizationId) {
console.error('❌ organization_id manquant');
return NextResponse.json(
{ error: 'organization_id est requis' },
{ status: 400 }
);
}
let actualOrgId = organizationId;
console.log('🔍 Récupération du nom du client depuis organizations...');
try {
const supabase = createSbServiceRole();
// 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();
const { data: orgData, error: orgError } = await supabase
.from('organizations')
.select('name')
.eq('id', organizationId)
.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 (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

View file

@ -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 });
}
}

View file

@ -302,6 +302,9 @@ export async function POST(req: Request) {
xml += ' <SvcLvl>\n';
xml += ' <Cd>SEPA</Cd>\n';
xml += ' </SvcLvl>\n';
xml += ' <CtgyPurp>\n';
xml += ' <Cd>SALA</Cd>\n';
xml += ' </CtgyPurp>\n';
xml += ' </PmtTpInf>\n';
xml += ` <ReqdExctnDt>${requestedExecutionDate}</ReqdExctnDt>\n`;
xml += ' <Dbtr>\n';

View file

@ -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<EmailTypeV2, EmailTemplateV2> = {
}
},
'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\'<strong>Odentas gère désormais vos virements de salaires</strong>.',
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.<br><br>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 <strong>vous gérez désormais vous-même vos virements de salaires</strong>.',
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é',