feat: Signatures électroniques docuseal [#22]

This commit is contained in:
N3WT DE COMPET
2025-02-28 18:30:18 +01:00
parent 8897d523dc
commit c8c8941ec8
41 changed files with 984 additions and 549 deletions

View File

@ -0,0 +1,200 @@
import React, { useState, useEffect } from 'react';
import ToggleSwitch from '@/components/ToggleSwitch'; // Import du composant ToggleSwitch
import { fetchRegistrationFileGroups } from '@/app/actions/registerFileGroupAction';
import DocusealBuilder from '@/components/DocusealBuilder'; // Import du composant wrapper
import logger from '@/utils/logger';
import { BE_DOCUSEAL_GET_JWT, BASE_URL } from '@/utils/Url';
import Button from '@/components/Button'; // Import du composant Button
import MultiSelect from '@/components/MultiSelect'; // Import du composant MultiSelect
import { createRegistrationTemplates } from '@/app/actions/subscriptionAction'; // Import de la fonction createRegistrationTemplates
import { useCsrfToken } from '@/context/CsrfContext';
export default function FileUpload({ handleCreateTemplateMaster, fileToEdit = null }) {
const [isRequired, setIsRequired] = useState(false); // État pour le toggle isRequired
const [order, setOrder] = useState(0);
const [groups, setGroups] = useState([]);
const [token, setToken] = useState(null);
const [templateMaster, setTemplateMaster] = useState(null);
const [uploadedFileName, setUploadedFileName] = useState('');
const [selectedGroups, setSelectedGroups] = useState([]);
const [guardianEmails, setGuardianEmails] = useState([]);
const [registrationFormIds, setRegistrationFormIds] = useState([]);
const csrfToken = useCsrfToken();
useEffect(() => {
fetchRegistrationFileGroups().then(data => setGroups(data));
if (fileToEdit) {
setUploadedFileName(fileToEdit.name || '');
setSelectedGroups(fileToEdit.groups || []);
}
}, [fileToEdit]);
useEffect(() => {
const body = fileToEdit
? JSON.stringify({
user_email: 'n3wt.school@gmail.com',
template_id: fileToEdit.template_id
})
: JSON.stringify({
user_email: 'n3wt.school@gmail.com'
});
fetch('/api/docuseal/generateToken', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: body,
})
.then((response) => response.json())
.then((data) => {
setToken(data.token);
})
.catch((error) => console.error(error));
}, [fileToEdit]);
const handleFileNameChange = (event) => {
setUploadedFileName(event.target.value);
};
const handleGroupChange = (selectedGroups) => {
setSelectedGroups(selectedGroups);
const emails = selectedGroups.flatMap(group => group.registration_forms.flatMap(form => form.guardians.map(guardian => guardian.email)));
setGuardianEmails(emails); // Mettre à jour la variable d'état avec les emails des guardians
const registrationFormIds = selectedGroups.flatMap(group => group.registration_forms.map(form => form.student_id));
setRegistrationFormIds(registrationFormIds); // Mettre à jour la variable d'état avec les IDs des dossiers d'inscription
logger.debug('Emails des Guardians associés aux groupes sélectionnés:', emails);
logger.debug('IDs des dossiers d\'inscription associés aux groupes sélectionnés:', registrationFormIds);
};
const handleLoad = (detail) => {
const templateId = detail?.id;
setTemplateMaster(detail);
logger.debug('Master template created with ID:', templateId);
}
const handleUpload = (detail) => {
logger.debug('Uploaded file detail:', detail);
setUploadedFileName(detail.name);
};
const handleSubmit = () => {
logger.debug('Création du template master:', templateMaster?.id);
handleCreateTemplateMaster({
name: uploadedFileName,
group_ids: selectedGroups.map(group => group.id),
template_id: templateMaster?.id
});
guardianEmails.forEach((email, index) => {
cloneTemplate(templateMaster?.id, email)
.then(clonedDocument => {
// Sauvegarde des templates clonés dans la base de données
const data = {
name: `clone_${clonedDocument.id}`,
template_id: clonedDocument.id,
master: templateMaster?.id,
registration_form: registrationFormIds[index]
};
createRegistrationTemplates(data, csrfToken)
.then(response => {
logger.debug('Template enregistré avec succès:', response);
})
.catch(error => {
logger.error('Erreur lors de l\'enregistrement du template:', error);
});
// Logique pour envoyer chaque template au submitter
logger.debug('Sending template to:', email);
})
.catch(error => {
logger.error('Error during cloning or sending:', error);
});
});
};
const cloneTemplate = (templateId, email) => {
return fetch('/api/docuseal/cloneTemplate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
templateId,
email
})
})
.then(response => {
if (!response.ok) {
return response.json().then(err => { throw new Error(err.message); });
}
return response.json();
})
.then(data => {
logger.debug('Template cloned successfully:', data);
return data;
})
.catch(error => {
console.error('Error cloning template:', error);
throw error;
});
};
return (
<div className="h-full flex flex-col mt-4 space-y-4">
<div className="grid grid-cols-10 gap-4 items-start">
<div className="col-span-2">
<MultiSelect
name="groups"
label="Sélection de groupes de fichiers"
options={groups}
selectedOptions={selectedGroups}
onChange={handleGroupChange}
errorMsg={null}
/>
</div>
<div className="col-span-7">
{token && (
<DocusealBuilder
token={token}
headers={{
'Authorization': `Bearer ${token}`
}}
withSendButton={false}
withSignYourselfButton={false}
autosave={true}
language={'fr'}
onLoad={handleLoad}
onUpload={handleUpload}
className="h-full overflow-auto" // Ajouter overflow-auto pour permettre le défilement
style={{ maxHeight: '70vh' }} // Limiter la hauteur maximale du composant
// Il faut auter l'host correspondant (une fois passé en HTTPS)
//host="docuseal:3001"
/>
)}
</div>
<div className="col-span-1 flex justify-end">
<Button
text="Valider"
onClick={handleSubmit}
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
(uploadedFileName === '' || selectedGroups.length === 0)
? "bg-gray-300 text-gray-700 cursor-not-allowed"
: "bg-emerald-500 text-white hover:bg-emerald-600"
}`}
primary
disabled={uploadedFileName === '' || selectedGroups.length === 0}
/>
</div>
</div>
</div>
);
}

View File

@ -2,14 +2,15 @@ import React, { useState, useEffect } from 'react';
import { Plus, Download, Edit, Trash2, FolderPlus, Signature } from 'lucide-react';
import Modal from '@/components/Modal';
import Table from '@/components/Table';
import FileUpload from '@/components/FileUpload';
import FileUpload from '@/components/Structure/Files/FileUpload';
import { formatDate } from '@/utils/Date';
import { BASE_URL } from '@/utils/Url';
import {
fetchRegisterFormFileTemplate,
createRegistrationFormFileTemplate,
editRegistrationFormFileTemplate,
deleteRegisterFormFileTemplate
fetchRegistrationTemplateMaster,
createRegistrationTemplateMaster,
editRegistrationTemplateMaster,
deleteRegistrationTemplateMaster,
getRegisterFormFileTemplate
} from '@/app/actions/subscriptionAction';
import {
fetchRegistrationFileGroups,
@ -17,10 +18,9 @@ import {
deleteRegistrationFileGroup,
editRegistrationFileGroup
} from '@/app/actions/registerFileGroupAction';
import RegistrationFileGroupForm from '@/components/RegistrationFileGroupForm';
import logger from '@/utils/logger';
import RegistrationFileGroupForm from '@/components/Structure/Files/RegistrationFileGroupForm';
export default function FilesManagement({ csrfToken }) {
export default function FilesGroupsManagement({ csrfToken }) {
const [fichiers, setFichiers] = useState([]);
const [groups, setGroups] = useState([]);
const [selectedGroup, setSelectedGroup] = useState(null);
@ -29,23 +29,18 @@ export default function FilesManagement({ csrfToken }) {
const [fileToEdit, setFileToEdit] = useState(null);
const [isGroupModalOpen, setIsGroupModalOpen] = useState(false);
const [groupToEdit, setGroupToEdit] = useState(null);
const [token, setToken] = useState(null);
const [selectedFile, setSelectedFile] = useState(null);
// Fonction pour transformer les données des fichiers avec les informations complètes du groupe
const transformFileData = (file, groups) => {
if (!file.group) return file;
const groupInfo = groups.find(g => g.id === file.group);
const groupInfos = file.groups.map(groupId => groups.find(g => g.id === groupId) || { id: groupId, name: 'Groupe inconnu' });
return {
...file,
group: groupInfo || { id: file.group, name: 'Groupe inconnu' }
groups: groupInfos
};
};
useEffect(() => {
Promise.all([
fetchRegisterFormFileTemplate(),
fetchRegistrationTemplateMaster(),
fetchRegistrationFileGroups()
]).then(([filesData, groupsData]) => {
setGroups(groupsData);
@ -57,12 +52,12 @@ export default function FilesManagement({ csrfToken }) {
const transformedFiles = filesData.map(file => transformFileData(file, groupsData));
setFichiers(transformedFiles);
}).catch(err => {
logger.debug(err.message);
console.log(err.message);
});
}, []);
const handleFileDelete = (fileId) => {
deleteRegisterFormFileTemplate(fileId, csrfToken)
deleteRegistrationTemplateMaster(fileId, csrfToken)
.then(response => {
if (response.ok) {
setFichiers(fichiers.filter(fichier => fichier.id !== fileId));
@ -72,7 +67,7 @@ export default function FilesManagement({ csrfToken }) {
}
})
.catch(error => {
logger.error('Error deleting file:', error);
console.error('Error deleting file:', error);
alert('Erreur lors de la suppression du fichier.');
});
};
@ -83,7 +78,45 @@ export default function FilesManagement({ csrfToken }) {
setIsModalOpen(true);
};
const handleFileUpload = ({file, name, is_required, order, groupId}) => {
const handleCreateTemplateMaster = ({name, group_ids, template_id}) => {
const data = {
name: name,
template_id: template_id,
groups: group_ids
};
console.log(data);
if (isEditing && fileToEdit) {
editRegistrationTemplateMaster(fileToEdit.id, data, csrfToken)
.then(data => {
// Transformer le fichier mis à jour avec les informations du groupe
const transformedFile = transformFileData(data, groups);
setFichiers(prevFichiers =>
prevFichiers.map(f => f.id === fileToEdit.id ? transformedFile : f)
);
setIsModalOpen(false);
setFileToEdit(null);
setIsEditing(false);
})
.catch(error => {
console.error('Error editing file:', error);
alert('Erreur lors de la modification du fichier');
});
} else {
createRegistrationTemplateMaster(data, csrfToken)
.then(data => {
// Transformer le nouveau fichier avec les informations du groupe
const transformedFile = transformFileData(data, groups);
setFichiers(prevFiles => [...prevFiles, transformedFile]);
setIsModalOpen(false);
})
.catch(error => {
console.error('Error uploading file:', error);
});
}
};
const handleTemplate = ({name, is_required, order, groupId, document_id}) => {
if (!name) {
alert('Veuillez entrer un nom de fichier.');
return;
@ -96,14 +129,16 @@ export default function FilesManagement({ csrfToken }) {
formData.append('name', name);
formData.append('is_required', is_required);
formData.append('order', order);
formData.append('document_id', document_id);
// Modification ici : vérifier si groupId existe et n'est pas vide
if (groupId && groupId !== '') {
formData.append('group', groupId); // Notez que le nom du champ est 'group' et non 'group_id'
}
if (isEditing && fileToEdit) {
editRegistrationFormFileTemplate(fileToEdit.id, formData, csrfToken)
editRegistrationTemplateMaster(fileToEdit.id, formData, csrfToken)
.then(data => {
// Transformer le fichier mis à jour avec les informations du groupe
const transformedFile = transformFileData(data, groups);
@ -115,11 +150,11 @@ export default function FilesManagement({ csrfToken }) {
setIsEditing(false);
})
.catch(error => {
logger.error('Error editing file:', error);
console.error('Error editing file:', error);
alert('Erreur lors de la modification du fichier');
});
} else {
createRegistrationFormFileTemplate(formData, csrfToken)
createRegistrationTemplateMaster(formData, csrfToken)
.then(data => {
// Transformer le nouveau fichier avec les informations du groupe
const transformedFile = transformFileData(data, groups);
@ -127,25 +162,33 @@ export default function FilesManagement({ csrfToken }) {
setIsModalOpen(false);
})
.catch(error => {
logger.error('Error uploading file:', error);
console.error('Error uploading file:', error);
});
}
};
const handleGroupSubmit = async (groupData) => {
try {
if (groupToEdit) {
const updatedGroup = await editRegistrationFileGroup(groupToEdit.id, groupData, csrfToken);
setGroups(groups.map(group => group.id === groupToEdit.id ? updatedGroup : group));
setGroupToEdit(null);
} else {
const newGroup = await createRegistrationFileGroup(groupData, csrfToken);
setGroups([...groups, newGroup]);
}
setIsGroupModalOpen(false);
} catch (error) {
logger.error('Error handling group:', error);
alert('Erreur lors de l\'opération sur le groupe');
const handleGroupSubmit = (groupData) => {
if (groupToEdit) {
editRegistrationFileGroup(groupToEdit.id, groupData, csrfToken)
.then(updatedGroup => {
setGroups(groups.map(group => group.id === groupToEdit.id ? updatedGroup : group));
setGroupToEdit(null);
setIsGroupModalOpen(false);
})
.catch(error => {
console.error('Error handling group:', error);
alert('Erreur lors de l\'opération sur le groupe');
});
} else {
createRegistrationFileGroup(groupData, csrfToken)
.then(newGroup => {
setGroups([...groups, newGroup]);
setIsGroupModalOpen(false);
})
.catch(error => {
console.error('Error handling group:', error);
alert('Erreur lors de l\'opération sur le groupe');
});
}
};
@ -175,24 +218,20 @@ export default function FilesManagement({ csrfToken }) {
alert('Groupe supprimé avec succès.');
})
.catch(error => {
logger.error('Error deleting group:', error);
console.error('Error deleting group:', error);
alert(error.message || 'Erreur lors de la suppression du groupe. Vérifiez qu\'aucune inscription n\'utilise ce groupe.');
});
}
};
// Ajouter cette fonction de filtrage
const filteredFiles = fichiers.filter(file => {
if (!selectedGroup) return true;
return file.group && file.group.id === parseInt(selectedGroup);
return file.groups && file.groups.some(group => group.id === parseInt(selectedGroup));
});
const columnsFiles = [
{ name: 'Nom du fichier', transform: (row) => row.name },
{ name: 'Groupe', transform: (row) => row.group ? row.group.name : 'Aucun' },
{ name: 'Date de création', transform: (row) => formatDate(new Date (row.date_added),"DD/MM/YYYY hh:mm:ss") },
{ name: 'Fichier Obligatoire', transform: (row) => row.is_required ? 'Oui' : 'Non' },
{ name: 'Ordre de fusion', transform: (row) => row.order },
{ name: 'Groupes', transform: (row) => row.groups && row.groups.length > 0 ? row.groups.map(group => group.name).join(', ') : 'Aucun' },
{ name: 'Actions', transform: (row) => (
<div className="flex items-center justify-center gap-2">
{row.file && (
@ -206,9 +245,6 @@ export default function FilesManagement({ csrfToken }) {
<button onClick={() => handleFileDelete(row.id)} className="text-red-500 hover:text-red-700">
<Trash2 size={16} />
</button>
<button onClick={() => handleSignatureRequest(row)} className="text-green-500 hover:text-green-700">
<Signature size={16} />
</button>
</div>
)}
];
@ -228,40 +264,24 @@ export default function FilesManagement({ csrfToken }) {
)}
];
// Fonction pour gérer la demande de signature
const handleSignatureRequest = (file) => {
fetch('http://localhost:8080/DocuSeal/generateToken', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
document_id: file.id,
user_email: 'anthony.casini.30@gmail.com',
url: file.file
}),
})
.then((response) => response.json())
.then((data) => {
console.log("token received : ", data.token);
setToken(data.token);
setSelectedFile(file);
})
.catch((error) => console.error(error));
};
return (
<div>
<Modal
isOpen={isModalOpen}
setIsOpen={setIsModalOpen}
title={isEditing ? 'Modifier un fichier' : 'Ajouter un fichier'}
setIsOpen={(isOpen) => {
setIsModalOpen(isOpen);
if (!isOpen) {
setFileToEdit(null);
}
}}
title={isEditing ? 'Modification du document' : 'Ajouter un document'}
ContentComponent={() => (
<FileUpload
onFileUpload={handleFileUpload}
handleCreateTemplateMaster={handleCreateTemplateMaster}
fileToEdit={fileToEdit}
/>
)}
modalClassName='w-4/5 h-4/5'
/>
<Modal
isOpen={isGroupModalOpen}
@ -324,14 +344,6 @@ export default function FilesManagement({ csrfToken }) {
/>
</div>
)}
{token && selectedFile && (
<DocusealBuilder
token={token}
headers={{
'Authorization': `Bearer Rh2CC75ZMZqirmtBGA5NRjUzj8hr9eDYTBeZxv3jgzb`
}}
/>
)}
</div>
);
}

View File

@ -0,0 +1,56 @@
import React, { useState, useEffect } from 'react';
export default function RegistrationFileGroupForm({ onSubmit, initialData }) {
const [name, setName] = useState('');
const [description, setDescription] = useState('');
useEffect(() => {
if (initialData) {
setName(initialData.name);
setDescription(initialData.description);
}
}, [initialData]);
const handleSubmit = (e) => {
e.preventDefault();
onSubmit({ name, description });
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nom du groupe
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Description
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div className="flex justify-end">
<button
type="submit"
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
{initialData ? 'Modifier le groupe' : 'Créer le groupe'}
</button>
</div>
</form>
);
}

View File

@ -0,0 +1,21 @@
import React, { useEffect, useState } from 'react';
import { fetchRegistrationFileGroups } from '@/app/actions/registerFileGroupAction';
export default function RegistrationFileGroupList() {
const [groups, setGroups] = useState([]);
useEffect(() => {
fetchRegistrationFileGroups().then(data => setGroups(data));
}, []);
return (
<div>
<h2>Groupes de fichiers d&apos;inscription</h2>
<ul>
{groups.map(group => (
<li key={group.id}>{group.name}</li>
))}
</ul>
</div>
);
}