chore: merge conflicts

This commit is contained in:
N3WT DE COMPET
2025-02-23 19:08:13 +01:00
parent 1911f79f45
commit 445cf35382
11 changed files with 187 additions and 166 deletions

View File

@ -66,6 +66,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'N3wtSchool.middleware.ContentSecurityPolicyMiddleware'
] ]
@ -261,7 +262,6 @@ CSRF_COOKIE_HTTPONLY = False
CSRF_COOKIE_SECURE = False CSRF_COOKIE_SECURE = False
CSRF_COOKIE_NAME = 'csrftoken' CSRF_COOKIE_NAME = 'csrftoken'
USE_TZ = True USE_TZ = True
TZ_APPLI = 'Europe/Paris' TZ_APPLI = 'Europe/Paris'
@ -328,3 +328,10 @@ SIMPLE_JWT = {
'TOKEN_TYPE_CLAIM': 'token_type', 'TOKEN_TYPE_CLAIM': 'token_type',
} }
# Configuration for DocuSeal JWT
DOCUSEAL_JWT = {
'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY,
'EXPIRATION_DELTA': timedelta(hours=1),
'API_KEY': '1kzHsXqN8P2ezUGT7TjVuBwM1hqtLsztrVSsQ87T7Mz'
}

View File

@ -44,6 +44,7 @@ urlpatterns = [
path("GestionMessagerie/", include(("GestionMessagerie.urls", 'GestionMessagerie'), namespace='GestionMessagerie')), path("GestionMessagerie/", include(("GestionMessagerie.urls", 'GestionMessagerie'), namespace='GestionMessagerie')),
path("GestionNotification/", include(("GestionNotification.urls", 'GestionNotification'), namespace='GestionNotification')), path("GestionNotification/", include(("GestionNotification.urls", 'GestionNotification'), namespace='GestionNotification')),
path("School/", include(("School.urls", 'School'), namespace='School')), path("School/", include(("School.urls", 'School'), namespace='School')),
path("DocuSeal/", include(("DocuSeal.urls", 'DocuSeal'), namespace='DocuSeal')),
# Documentation Api # Documentation Api
re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'), re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),

View File

@ -25,6 +25,18 @@ const nextConfig = {
AUTH_SECRET: process.env.AUTH_SECRET || 'false', AUTH_SECRET: process.env.AUTH_SECRET || 'false',
NEXTAUTH_URL: process.env.NEXTAUTH_URL || "http://localhost:3000", NEXTAUTH_URL: process.env.NEXTAUTH_URL || "http://localhost:3000",
}, },
async rewrites() {
return [
{
source: '/api/documents/:path*',
destination: 'https://api.docuseal.com/v1/documents/:path*',
},
{
source: '/api/auth/:path*',
destination: '/api/auth/:path*', // Exclure les routes NextAuth des réécritures de proxy
},
];
}
}; };
export default withNextIntl(nextConfig); export default withNextIntl(nextConfig);

View File

@ -8,6 +8,7 @@
"name": "n3wt-school-front-end", "name": "n3wt-school-front-end",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@docuseal/react": "^1.0.56",
"@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2",
"@tailwindcss/forms": "^0.5.9", "@tailwindcss/forms": "^0.5.9",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
@ -201,6 +202,12 @@
"kuler": "^2.0.0" "kuler": "^2.0.0"
} }
}, },
"node_modules/@docuseal/react": {
"version": "1.0.56",
"resolved": "https://registry.npmjs.org/@docuseal/react/-/react-1.0.56.tgz",
"integrity": "sha512-xna62Op4WLIVmgz2U0mi4paFayslxBUk2P8u3D70e1JgVRXsPFwzH6b1WhotedN9PMPS+cG2HP1PmpYoEzdZTQ==",
"license": "MIT"
},
"node_modules/@eslint-community/eslint-utils": { "node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0", "version": "4.4.0",
"dev": true, "dev": true,

View File

@ -10,6 +10,7 @@
"check-strings": "node scripts/check-hardcoded-strings.js" "check-strings": "node scripts/check-hardcoded-strings.js"
}, },
"dependencies": { "dependencies": {
"@docuseal/react": "^1.0.56",
"@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2",
"@tailwindcss/forms": "^0.5.9", "@tailwindcss/forms": "^0.5.9",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",

View File

@ -1,7 +1,7 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
export default function InputPhone({ name, label, value, onChange, errorMsg, placeholder, className }) { export default function InputPhone({ name, label, value, onChange, errorMsg, placeholder, className, required }) {
const inputRef = useRef(null); const inputRef = useRef(null);
useEffect(() => { useEffect(() => {
@ -18,7 +18,10 @@ export default function InputPhone({ name, label, value, onChange, errorMsg, pla
return ( return (
<> <>
<div className={`mb-4 ${className}`}> <div className={`mb-4 ${className}`}>
<label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label> <label htmlFor={name} className="block text-sm font-medium text-gray-700">
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</label>
<div className={`mt-1 flex items-center border border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500`}> <div className={`mt-1 flex items-center border border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500`}>
<input <input
type="tel" type="tel"

View File

@ -15,14 +15,9 @@ import DraggableFileUpload from '@/components/DraggableFileUpload';
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
import FileStatusLabel from '@/components/FileStatusLabel'; import FileStatusLabel from '@/components/FileStatusLabel';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import StudentInfoForm from '@/components/Inscription/StudentInfoForm';
// Définition des niveaux scolaires disponibles import FilesToSign from '@/components/Inscription/FilesToSign';
const levels = [ import FilesToUpload from '@/components/Inscription/FilesToUpload';
{ value:'1', label: 'TPS - Très Petite Section'},
{ value:'2', label: 'PS - Petite Section'},
{ value:'3', label: 'MS - Moyenne Section'},
{ value:'4', label: 'GS - Grande Section'},
];
/** /**
* Composant de formulaire d'inscription partagé * Composant de formulaire d'inscription partagé
@ -65,6 +60,8 @@ export default function InscriptionFormShared({
const [showUploadModal, setShowUploadModal] = useState(false); const [showUploadModal, setShowUploadModal] = useState(false);
const [currentTemplateId, setCurrentTemplateId] = useState(null); const [currentTemplateId, setCurrentTemplateId] = useState(null);
const [currentPage, setCurrentPage] = useState(1);
// Chargement initial des données // Chargement initial des données
// Mettre à jour les données quand initialData change // Mettre à jour les données quand initialData change
useEffect(() => { useEffect(() => {
@ -185,6 +182,21 @@ export default function InscriptionFormShared({
return errors?.student?.[field]?.[0]; return errors?.student?.[field]?.[0];
}; };
const handleNextPage = () => {
setCurrentPage(currentPage + 1);
};
const handlePreviousPage = () => {
setCurrentPage(currentPage - 1);
};
const requiredFileTemplates = fileTemplates.filter(template => template.is_required);
// Ajout des logs pour débogage
console.log('BASE_URL:', BASE_URL);
console.log('requiredFileTemplates:', requiredFileTemplates);
console.log('currentPage:', currentPage);
// Configuration des colonnes pour le tableau des fichiers // Configuration des colonnes pour le tableau des fichiers
const columns = [ const columns = [
{ name: 'Nom du fichier', transform: (row) => row.name }, { name: 'Nom du fichier', transform: (row) => row.name },
@ -248,128 +260,54 @@ export default function InscriptionFormShared({
<div className="max-w-4xl mx-auto p-6"> <div className="max-w-4xl mx-auto p-6">
<form onSubmit={handleSubmit} className="space-y-8"> <form onSubmit={handleSubmit} className="space-y-8">
<DjangoCSRFToken csrfToken={csrfToken}/> <DjangoCSRFToken csrfToken={csrfToken}/>
{/* Section Élève */} {/* Page 1 : Informations de l'élève et Responsables */}
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> {currentPage === 1 && (
<h2 className="text-xl font-bold mb-4 text-gray-800">Informations de l&apos;élève</h2> <StudentInfoForm
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> formData={formData}
<InputText updateFormField={updateFormField}
name="last_name"
label="Nom"
value={formData.last_name}
onChange={(e) => updateFormField('last_name', e.target.value)}
required
errorMsg={getError('last_name')}
/>
<InputText
name="first_name"
label="Prénom"
value={formData.first_name}
onChange={(e) => updateFormField('first_name', e.target.value)}
errorMsg={getError('first_name')}
required
/>
<InputText
name="nationality"
label="Nationalité"
value={formData.nationality}
onChange={(e) => updateFormField('nationality', e.target.value)}
/>
<InputText
name="birth_date"
type="date"
label="Date de Naissance"
value={formData.birth_date}
onChange={(e) => updateFormField('birth_date', e.target.value)}
required
errorMsg={getError('birth_date')}
/>
<InputText
name="birth_place"
label="Lieu de Naissance"
value={formData.birth_place}
onChange={(e) => updateFormField('birth_place', e.target.value)}
errorMsg={getError('birth_place')}
/>
<InputText
name="birth_postal_code"
label="Code Postal de Naissance"
value={formData.birth_postal_code}
onChange={(e) => updateFormField('birth_postal_code', e.target.value)}
required
errorMsg={getError('birth_postal_code')}
/>
<div className="md:col-span-2">
<InputText
name="address"
label="Adresse"
value={formData.address}
onChange={(e) => updateFormField('address', e.target.value)}
errorMsg={getError('address')}
/>
</div>
<InputText
name="attending_physician"
label="Médecin Traitant"
value={formData.attending_physician}
onChange={(e) => updateFormField('attending_physician', e.target.value)}
errorMsg={getError('attending_physician')}
/>
<SelectChoice
name="level"
label="Niveau"
placeHolder="Sélectionner un niveau"
selected={formData.level}
callback={(e) => updateFormField('level', e.target.value)}
choices={levels}
required
errorMsg={getError('level')}
/>
</div>
</div>
{/* Section Responsables */}
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-bold mb-4 text-gray-800">Responsables</h2>
<ResponsableInputFields
guardians={guardians} guardians={guardians}
onGuardiansChange={(id, field, value) => { setGuardians={setGuardians}
const updatedGuardians = guardians.map(resp => errors={errors}
resp.id === id ? { ...resp, [field]: value } : resp
);
setGuardians(updatedGuardians);
}}
addGuardian={(e) => {
e.preventDefault();
setGuardians([...guardians, { id: Date.now() }]);
}}
deleteGuardian={(index) => {
const newArray = [...guardians];
newArray.splice(index, 1);
setGuardians(newArray);
}}
errors={errors?.student?.guardians || []}
/> />
</div> )}
{/* Section Fichiers d'inscription */} {/* Pages suivantes : Section Fichiers d'inscription */}
{fileTemplates.length > 0 && ( {currentPage > 1 && currentPage <= requiredFileTemplates.length + 1 && (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> <div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-bold mb-4 text-gray-800">Fichiers à remplir</h2> <h2 className="text-xl font-bold mb-4 text-gray-800">{requiredFileTemplates[currentPage - 2].name}</h2>
<Table <iframe
data={fileTemplates} src={`${BASE_URL}/data/${requiredFileTemplates[currentPage - 2].file}?signature=true`}
columns={columns} width="100%"
itemsPerPage={5} height="800px"
currentPage={1} className="w-full" // Utiliser la classe CSS pour la largeur
totalPages={1} title={requiredFileTemplates[currentPage - 2].name}
onPageChange={() => {}} >
/> <p>Votre navigateur ne prend pas en charge les fichiers PDF. Vous pouvez télécharger le fichier en cliquant <a href={`${BASE_URL}/data/${requiredFileTemplates[currentPage - 2].file}`}>ici</a>.</p>
</iframe>
</div> </div>
)} )}
{/* Dernière page : Section Fichiers parents */}
{currentPage === requiredFileTemplates.length + 2 && (
<>
<FilesToUpload
fileTemplates={fileTemplates.filter(template => !template.is_required)}
columns={columns}
/>
</>
)}
{/* Boutons de contrôle */} {/* Boutons de contrôle */}
<div className="flex justify-end space-x-4"> <div className="flex justify-end space-x-4">
<Button href={cancelUrl} text="Annuler" /> {currentPage > 1 && (
<Button text="Précédent" onClick={(e) => { e.preventDefault(); handlePreviousPage(); }} />
)}
{currentPage < requiredFileTemplates.length + 2 && (
<Button text="Suivant" onClick={(e) => { e.preventDefault(); handleNextPage(); }} />
)}
{currentPage === requiredFileTemplates.length + 2 && (
<Button type="submit" text="Valider" primary /> <Button type="submit" text="Valider" primary />
)}
</div> </div>
</form> </form>
{fileTemplates.length > 0 && ( {fileTemplates.length > 0 && (

View File

@ -58,6 +58,7 @@ export default function ResponsableInputFields({guardians, onGuardiansChange, ad
label={t('email')} label={t('email')}
value={item.email} value={item.email}
onChange={(event) => {onGuardiansChange(item.id, "email", event.target.value)}} onChange={(event) => {onGuardiansChange(item.id, "email", event.target.value)}}
required
errorMsg={getError(index, 'email')} errorMsg={getError(index, 'email')}
/> />
<InputPhone <InputPhone
@ -65,6 +66,7 @@ export default function ResponsableInputFields({guardians, onGuardiansChange, ad
label={t('phone')} label={t('phone')}
value={item.phone} value={item.phone}
onChange={(event) => {onGuardiansChange(item.id, "phone", event)}} onChange={(event) => {onGuardiansChange(item.id, "phone", event)}}
required
errorMsg={getError(index, 'phone')} errorMsg={getError(index, 'phone')}
/> />
</div> </div>
@ -76,6 +78,7 @@ export default function ResponsableInputFields({guardians, onGuardiansChange, ad
label={t('birthdate')} label={t('birthdate')}
value={item.birth_date} value={item.birth_date}
onChange={(event) => {onGuardiansChange(item.id, "birth_date", event.target.value)}} onChange={(event) => {onGuardiansChange(item.id, "birth_date", event.target.value)}}
required
errorMsg={getError(index, 'birth_date')} errorMsg={getError(index, 'birth_date')}
/> />
<InputText <InputText
@ -84,6 +87,7 @@ export default function ResponsableInputFields({guardians, onGuardiansChange, ad
label={t('profession')} label={t('profession')}
value={item.profession} value={item.profession}
onChange={(event) => {onGuardiansChange(item.id, "profession", event.target.value)}} onChange={(event) => {onGuardiansChange(item.id, "profession", event.target.value)}}
required
errorMsg={getError(index, 'profession')} errorMsg={getError(index, 'profession')}
/> />
</div> </div>
@ -95,6 +99,7 @@ export default function ResponsableInputFields({guardians, onGuardiansChange, ad
label={t('address')} label={t('address')}
value={item.address} value={item.address}
onChange={(event) => {onGuardiansChange(item.id, "address", event.target.value)}} onChange={(event) => {onGuardiansChange(item.id, "address", event.target.value)}}
required
errorMsg={getError(index, 'address')} errorMsg={getError(index, 'address')}
/> />
</div> </div>

View File

@ -1,6 +1,7 @@
export default function SelectChoice({ type, name, label, required, placeHolder, choices, callback, selected, errorMsg, IconItem, disabled = false }) { export default function SelectChoice({ type, name, label, required, placeHolder, choices, callback, selected, errorMsg, IconItem, disabled = false }) {
return ( return (
<> <>
<div>
<label htmlFor={name} className="block text-sm font-medium text-gray-700"> <label htmlFor={name} className="block text-sm font-medium text-gray-700">
{label} {label}
{required && <span className="text-red-500 ml-1">*</span>} {required && <span className="text-red-500 ml-1">*</span>}
@ -29,6 +30,7 @@ export default function SelectChoice({ type, name, label, required, placeHolder,
</select> </select>
</div> </div>
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>} {errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
</div>
</> </>
); );
} }

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Plus, Download, Edit, Trash2, FolderPlus } from 'lucide-react'; import { Plus, Download, Edit, Trash2, FolderPlus, Signature } from 'lucide-react';
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
import Table from '@/components/Table'; import Table from '@/components/Table';
import FileUpload from '@/components/FileUpload'; import FileUpload from '@/components/FileUpload';
@ -29,6 +29,8 @@ export default function FilesManagement({ csrfToken }) {
const [fileToEdit, setFileToEdit] = useState(null); const [fileToEdit, setFileToEdit] = useState(null);
const [isGroupModalOpen, setIsGroupModalOpen] = useState(false); const [isGroupModalOpen, setIsGroupModalOpen] = useState(false);
const [groupToEdit, setGroupToEdit] = useState(null); 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 // Fonction pour transformer les données des fichiers avec les informations complètes du groupe
const transformFileData = (file, groups) => { const transformFileData = (file, groups) => {
@ -204,6 +206,9 @@ export default function FilesManagement({ csrfToken }) {
<button onClick={() => handleFileDelete(row.id)} className="text-red-500 hover:text-red-700"> <button onClick={() => handleFileDelete(row.id)} className="text-red-500 hover:text-red-700">
<Trash2 size={16} /> <Trash2 size={16} />
</button> </button>
<button onClick={() => handleSignatureRequest(row)} className="text-green-500 hover:text-green-700">
<Signature size={16} />
</button>
</div> </div>
)} )}
]; ];
@ -223,6 +228,28 @@ 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 ( return (
<div> <div>
<Modal <Modal
@ -297,6 +324,14 @@ export default function FilesManagement({ csrfToken }) {
/> />
</div> </div>
)} )}
{token && selectedFile && (
<DocusealBuilder
token={token}
headers={{
'Authorization': `Bearer Rh2CC75ZMZqirmtBGA5NRjUzj8hr9eDYTBeZxv3jgzb`
}}
/>
)}
</div> </div>
); );
} }

View File

@ -19,6 +19,15 @@ services:
POSTGRES_DB: school POSTGRES_DB: school
TZ: Europe/Paris TZ: Europe/Paris
docuseal:
image: docuseal/docuseal:latest
depends_on:
- database
ports:
- 3001:3000
environment:
- DATABASE_URL=postgresql://postgres:postgres@database:5432/docuseal
backend: backend:
build: build:
context: ./Back-End context: ./Back-End
@ -37,19 +46,20 @@ services:
depends_on: depends_on:
- redis - redis
- database - database
- docuseal
command: python start.py command: python start.py
frontend: # frontend:
build: # build:
context: ./Front-End # context: ./Front-End
args: # args:
- BUILD_MODE=development # - BUILD_MODE=development
ports: # ports:
- 3000:3000 # - 3000:3000
volumes: # volumes:
- ./Front-End:/app # - ./Front-End:/app
environment: # environment:
- TZ=Europe/Paris # - TZ=Europe/Paris
depends_on: # depends_on:
- backend # - backend