mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-05 20:51:26 +00:00
feat: Finalisation formulaire dynamique
This commit is contained in:
225
Front-End/src/app/[locale]/admin/structure/FormBuilder/page.js
Normal file
225
Front-End/src/app/[locale]/admin/structure/FormBuilder/page.js
Normal file
@ -0,0 +1,225 @@
|
||||
'use client';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import FormTemplateBuilder from '@/components/Form/FormTemplateBuilder';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import {
|
||||
fetchRegistrationFileGroups,
|
||||
fetchRegistrationSchoolFileMasterById,
|
||||
createRegistrationSchoolFileMaster,
|
||||
editRegistrationSchoolFileMaster,
|
||||
} from '@/app/actions/registerFileGroupAction';
|
||||
import { getSecureFileUrl } from '@/utils/fileUrl';
|
||||
import logger from '@/utils/logger';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
import { FE_ADMIN_STRUCTURE_URL } from '@/utils/Url';
|
||||
|
||||
export default function FormBuilderPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const { selectedEstablishmentId } = useEstablishment();
|
||||
const csrfToken = useCsrfToken();
|
||||
const { showNotification } = useNotification();
|
||||
|
||||
const formId = searchParams.get('id');
|
||||
const preGroupId = searchParams.get('groupId');
|
||||
const isEditing = !!formId;
|
||||
|
||||
const [groups, setGroups] = useState([]);
|
||||
const [initialData, setInitialData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [uploadedFile, setUploadedFile] = useState(null);
|
||||
const [existingFileUrl, setExistingFileUrl] = useState(null);
|
||||
|
||||
const normalizeBackendFile = (rawFile, rawFileUrl) => {
|
||||
if (typeof rawFileUrl === 'string' && rawFileUrl.trim()) {
|
||||
return rawFileUrl;
|
||||
}
|
||||
|
||||
if (typeof rawFile === 'string' && rawFile.trim()) {
|
||||
return rawFile;
|
||||
}
|
||||
|
||||
if (rawFile && typeof rawFile === 'object') {
|
||||
if (typeof rawFile.url === 'string' && rawFile.url.trim()) {
|
||||
return rawFile.url;
|
||||
}
|
||||
if (typeof rawFile.path === 'string' && rawFile.path.trim()) {
|
||||
return rawFile.path;
|
||||
}
|
||||
if (typeof rawFile.name === 'string' && rawFile.name.trim()) {
|
||||
return rawFile.name;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const previewFileUrl = useMemo(() => {
|
||||
if (uploadedFile instanceof File) {
|
||||
return URL.createObjectURL(uploadedFile);
|
||||
}
|
||||
return existingFileUrl || null;
|
||||
}, [uploadedFile, existingFileUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (previewFileUrl && previewFileUrl.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(previewFileUrl);
|
||||
}
|
||||
};
|
||||
}, [previewFileUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedEstablishmentId) return;
|
||||
|
||||
Promise.all([
|
||||
fetchRegistrationFileGroups(selectedEstablishmentId),
|
||||
formId ? fetchRegistrationSchoolFileMasterById(formId) : Promise.resolve(null),
|
||||
])
|
||||
.then(([groupsData, formData]) => {
|
||||
setGroups(groupsData || []);
|
||||
if (formData) {
|
||||
setInitialData(formData);
|
||||
const resolvedFile = normalizeBackendFile(
|
||||
formData.file,
|
||||
formData.file_url
|
||||
);
|
||||
if (resolvedFile) {
|
||||
setExistingFileUrl(resolvedFile);
|
||||
}
|
||||
} else if (preGroupId) {
|
||||
setInitialData({ groups: [{ id: Number(preGroupId) }] });
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error('Error loading FormBuilder data:', err);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [selectedEstablishmentId, formId, preGroupId]);
|
||||
|
||||
const buildFormData = async (name, group_ids, formMasterData) => {
|
||||
const dataToSend = new FormData();
|
||||
dataToSend.append(
|
||||
'data',
|
||||
JSON.stringify({
|
||||
name,
|
||||
groups: group_ids,
|
||||
formMasterData,
|
||||
establishment: selectedEstablishmentId,
|
||||
})
|
||||
);
|
||||
|
||||
if (uploadedFile instanceof File) {
|
||||
const ext =
|
||||
uploadedFile.name.lastIndexOf('.') !== -1
|
||||
? uploadedFile.name.substring(uploadedFile.name.lastIndexOf('.'))
|
||||
: '';
|
||||
const cleanName = (name || 'document')
|
||||
.replace(/[^a-zA-Z0-9_\-]/g, '_')
|
||||
.replace(/_+/g, '_')
|
||||
.replace(/^_+|_+$/g, '');
|
||||
dataToSend.append('file', uploadedFile, `${cleanName}${ext}`);
|
||||
} else if (existingFileUrl && isEditing) {
|
||||
const lastDot = existingFileUrl.lastIndexOf('.');
|
||||
const ext = lastDot !== -1 ? existingFileUrl.substring(lastDot) : '';
|
||||
const cleanName = (name || 'document')
|
||||
.replace(/[^a-zA-Z0-9_\-]/g, '_')
|
||||
.replace(/_+/g, '_')
|
||||
.replace(/^_+|_+$/g, '');
|
||||
try {
|
||||
const resp = await fetch(getSecureFileUrl(existingFileUrl));
|
||||
if (resp.ok) {
|
||||
const blob = await resp.blob();
|
||||
dataToSend.append('file', blob, `${cleanName}${ext}`);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('Could not re-fetch existing file:', e);
|
||||
}
|
||||
}
|
||||
|
||||
return dataToSend;
|
||||
};
|
||||
|
||||
const handleSave = async ({ name, group_ids, formMasterData, id }) => {
|
||||
const hasFileField = (formMasterData?.fields || []).some(
|
||||
(field) => field.type === 'file'
|
||||
);
|
||||
const hasUploadedDocument =
|
||||
uploadedFile instanceof File || Boolean(existingFileUrl);
|
||||
|
||||
if (hasFileField && !hasUploadedDocument) {
|
||||
showNotification(
|
||||
'Un document PDF doit être uploadé si le formulaire contient un champ fichier.',
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const dataToSend = await buildFormData(name, group_ids, formMasterData);
|
||||
if (isEditing) {
|
||||
await editRegistrationSchoolFileMaster(id || formId, dataToSend, csrfToken);
|
||||
showNotification(
|
||||
`Le formulaire "${name}" a été modifié avec succès.`,
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
} else {
|
||||
await createRegistrationSchoolFileMaster(dataToSend, csrfToken);
|
||||
showNotification(
|
||||
`Le formulaire "${name}" a été créé avec succès.`,
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
}
|
||||
router.push(FE_ADMIN_STRUCTURE_URL);
|
||||
} catch (err) {
|
||||
logger.error('Error saving form:', err);
|
||||
showNotification('Erreur lors de la sauvegarde du formulaire', 'error', 'Erreur');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<p className="text-gray-500">Chargement...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-screen bg-neutral">
|
||||
{/* Header sticky */}
|
||||
<div className="sticky top-0 z-10 bg-white border-b border-gray-200 px-4 py-3 flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => router.push(FE_ADMIN_STRUCTURE_URL)}
|
||||
className="flex items-center gap-2 text-primary hover:text-secondary font-label font-medium transition-colors"
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
Retour
|
||||
</button>
|
||||
<h1 className="text-lg font-headline font-semibold text-gray-800">
|
||||
{isEditing ? 'Modifier le formulaire' : 'Créer un formulaire personnalisé'}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="max-w-5xl mx-auto px-4 py-6 space-y-4">
|
||||
{/* FormTemplateBuilder */}
|
||||
<FormTemplateBuilder
|
||||
onSave={handleSave}
|
||||
initialData={initialData}
|
||||
groups={groups}
|
||||
isEditing={isEditing}
|
||||
masterFile={previewFileUrl}
|
||||
onMasterFileUpload={(file) => setUploadedFile(file)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -26,6 +26,10 @@ export const fetchRegistrationSchoolFileMasters = (establishment) => {
|
||||
return fetchWithAuth(url);
|
||||
};
|
||||
|
||||
export const fetchRegistrationSchoolFileMasterById = (id) => {
|
||||
return fetchWithAuth(`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}/${id}`);
|
||||
};
|
||||
|
||||
export const fetchRegistrationParentFileMasters = (establishment) => {
|
||||
const url = `${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}?establishment_id=${establishment}`;
|
||||
return fetchWithAuth(url);
|
||||
|
||||
@ -3,6 +3,7 @@ import { useForm, Controller } from 'react-hook-form';
|
||||
import InputTextIcon from './InputTextIcon';
|
||||
import SelectChoice from './SelectChoice';
|
||||
import Button from './Button';
|
||||
import FileUpload from './FileUpload';
|
||||
import IconSelector from './IconSelector';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import { FIELD_TYPES } from './FormTypes';
|
||||
@ -14,6 +15,8 @@ export default function AddFieldModal({
|
||||
onSubmit,
|
||||
editingField = null,
|
||||
editingIndex = -1,
|
||||
hasMasterFile = false,
|
||||
onMasterFileUpload,
|
||||
}) {
|
||||
const isEditing = editingIndex >= 0;
|
||||
|
||||
@ -29,6 +32,7 @@ export default function AddFieldModal({
|
||||
acceptTypes: '',
|
||||
maxSize: 5, // 5MB par défaut
|
||||
checked: false,
|
||||
masterFileToUpload: null,
|
||||
validation: {
|
||||
pattern: '',
|
||||
minLength: '',
|
||||
@ -56,6 +60,7 @@ export default function AddFieldModal({
|
||||
acceptTypes: '',
|
||||
maxSize: 5,
|
||||
checked: false,
|
||||
masterFileToUpload: null,
|
||||
signatureData: '',
|
||||
backgroundColor: '#ffffff',
|
||||
penColor: '#000000',
|
||||
@ -492,6 +497,31 @@ export default function AddFieldModal({
|
||||
|
||||
{currentField.type === 'file' && (
|
||||
<>
|
||||
<div className="rounded border border-gray-200 bg-gray-50 p-3">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Document PDF du formulaire{' '}
|
||||
<span className="text-red-500">*</span>
|
||||
</label>
|
||||
<FileUpload
|
||||
selectionMessage="Uploader le PDF à afficher dans l'aperçu"
|
||||
onFileSelect={(file) => {
|
||||
setCurrentField((prev) => ({
|
||||
...prev,
|
||||
masterFileToUpload: file,
|
||||
}));
|
||||
if (onMasterFileUpload) {
|
||||
onMasterFileUpload(file);
|
||||
}
|
||||
}}
|
||||
enable
|
||||
/>
|
||||
{!hasMasterFile && !currentField.masterFileToUpload && (
|
||||
<p className="text-xs text-red-500 mt-2">
|
||||
Uploadez un document avant d'ajouter ce type de champ.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Controller
|
||||
name="acceptTypes"
|
||||
control={control}
|
||||
|
||||
@ -2,6 +2,7 @@ import logger from '@/utils/logger';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { useEffect } from 'react';
|
||||
import SelectChoice from './SelectChoice';
|
||||
import { getSecureFileUrl } from '@/utils/fileUrl';
|
||||
import InputTextIcon from './InputTextIcon';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import Button from './Button';
|
||||
@ -33,7 +34,22 @@ export default function FormRenderer({
|
||||
onFormSubmit = (data) => {
|
||||
alert(JSON.stringify(data, null, 2));
|
||||
}, // Callback de soumission personnalisé (optionnel)
|
||||
masterFile = null,
|
||||
}) {
|
||||
const resolveMasterFileUrl = (fileValue) => {
|
||||
if (!fileValue) return null;
|
||||
if (typeof fileValue !== 'string') return null;
|
||||
if (fileValue.startsWith('blob:')) return fileValue;
|
||||
if (fileValue.startsWith('data:')) return fileValue;
|
||||
if (fileValue.startsWith('http://') || fileValue.startsWith('https://')) {
|
||||
return fileValue;
|
||||
}
|
||||
if (fileValue.startsWith('/api/download?')) return fileValue;
|
||||
return getSecureFileUrl(fileValue);
|
||||
};
|
||||
|
||||
const masterFileUrl = resolveMasterFileUrl(masterFile);
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
@ -57,8 +73,7 @@ export default function FormRenderer({
|
||||
const hasFiles = Object.keys(data).some((key) => {
|
||||
return (
|
||||
data[key] instanceof FileList ||
|
||||
(data[key] && data[key][0] instanceof File) ||
|
||||
(typeof data[key] === 'string' && data[key].startsWith('data:image'))
|
||||
(data[key] && data[key][0] instanceof File)
|
||||
);
|
||||
});
|
||||
|
||||
@ -83,29 +98,6 @@ export default function FormRenderer({
|
||||
formData.append(`files.${key}`, value[i]);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
typeof value === 'string' &&
|
||||
value.startsWith('data:image')
|
||||
) {
|
||||
// Gérer les signatures (SVG ou images base64)
|
||||
if (value.includes('svg+xml')) {
|
||||
// Gérer les signatures SVG
|
||||
const svgData = value.split(',')[1];
|
||||
const svgBlob = new Blob([atob(svgData)], {
|
||||
type: 'image/svg+xml',
|
||||
});
|
||||
formData.append(`files.${key}`, svgBlob, `signature_${key}.svg`);
|
||||
} else {
|
||||
// Gérer les images base64 classiques
|
||||
const byteString = atob(value.split(',')[1]);
|
||||
const ab = new ArrayBuffer(byteString.length);
|
||||
const ia = new Uint8Array(ab);
|
||||
for (let i = 0; i < byteString.length; i++) {
|
||||
ia[i] = byteString.charCodeAt(i);
|
||||
}
|
||||
const blob = new Blob([ab], { type: 'image/png' });
|
||||
formData.append(`files.${key}`, blob, `signature_${key}.png`);
|
||||
}
|
||||
} else {
|
||||
// Gérer les autres types de champs
|
||||
formData.append(
|
||||
@ -356,24 +348,39 @@ export default function FormRenderer({
|
||||
control={control}
|
||||
rules={{ required: field.required }}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<FileUpload
|
||||
selectionMessage={field.label}
|
||||
required={field.required}
|
||||
uploadedFileName={value ? value[0]?.name : null}
|
||||
onFileSelect={(file) => {
|
||||
// Créer un objet de type FileList similaire pour la compatibilité
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file);
|
||||
onChange(dataTransfer.files);
|
||||
}}
|
||||
errorMsg={
|
||||
errors[field.id]
|
||||
? field.required
|
||||
? `${field.label} est requis`
|
||||
: 'Champ invalide'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
masterFileUrl ? (
|
||||
<div className="w-full bg-neutral border border-gray-200 rounded p-3">
|
||||
{field.label && (
|
||||
<p className="text-sm font-medium text-gray-700 mb-2">
|
||||
{field.label}
|
||||
</p>
|
||||
)}
|
||||
<iframe
|
||||
src={masterFileUrl}
|
||||
title={field.label || 'Document'}
|
||||
className="w-full rounded border border-gray-200 bg-white"
|
||||
style={{ height: '520px', border: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<FileUpload
|
||||
selectionMessage={field.label}
|
||||
required={field.required}
|
||||
uploadedFileName={value ? value[0]?.name : null}
|
||||
onFileSelect={(file) => {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file);
|
||||
onChange(dataTransfer.files);
|
||||
}}
|
||||
errorMsg={
|
||||
errors[field.id]
|
||||
? field.required
|
||||
? `${field.label} est requis`
|
||||
: 'Champ invalide'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
@ -406,7 +413,14 @@ export default function FormRenderer({
|
||||
control={control}
|
||||
rules={{ required: field.required }}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<div>
|
||||
<div
|
||||
className={
|
||||
masterFile
|
||||
? 'mt-3 flex justify-end'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<div className={masterFile ? 'w-full max-w-xs' : 'w-full'}>
|
||||
<SignatureField
|
||||
label={field.label}
|
||||
required={field.required}
|
||||
@ -415,6 +429,8 @@ export default function FormRenderer({
|
||||
backgroundColor={field.backgroundColor || '#ffffff'}
|
||||
penColor={field.penColor || '#000000'}
|
||||
penWidth={field.penWidth || 2}
|
||||
displayWidth={masterFile ? 260 : 400}
|
||||
displayHeight={masterFile ? 120 : 200}
|
||||
/>
|
||||
{errors[field.id] && (
|
||||
<p className="text-red-500 text-sm mt-1">
|
||||
@ -423,6 +439,7 @@ export default function FormRenderer({
|
||||
: 'Champ invalide'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -177,6 +177,8 @@ export default function FormTemplateBuilder({
|
||||
initialData,
|
||||
groups,
|
||||
isEditing,
|
||||
masterFile = null,
|
||||
onMasterFileUpload,
|
||||
}) {
|
||||
const [formConfig, setFormConfig] = useState({
|
||||
id: initialData?.id || 0,
|
||||
@ -186,7 +188,9 @@ export default function FormTemplateBuilder({
|
||||
});
|
||||
|
||||
const [selectedGroups, setSelectedGroups] = useState(
|
||||
initialData?.groups?.map((g) => g.id) || []
|
||||
initialData?.groups?.map((g) =>
|
||||
typeof g === 'object' && g !== null ? g.id : g
|
||||
) || []
|
||||
);
|
||||
|
||||
const [showAddFieldModal, setShowAddFieldModal] = useState(false);
|
||||
@ -209,7 +213,11 @@ export default function FormTemplateBuilder({
|
||||
submitLabel: 'Envoyer',
|
||||
fields: initialData.formMasterData?.fields || [],
|
||||
});
|
||||
setSelectedGroups(initialData.groups?.map((g) => g.id) || []);
|
||||
setSelectedGroups(
|
||||
initialData.groups?.map((g) =>
|
||||
typeof g === 'object' && g !== null ? g.id : g
|
||||
) || []
|
||||
);
|
||||
}
|
||||
}, [initialData]);
|
||||
|
||||
@ -256,6 +264,21 @@ export default function FormTemplateBuilder({
|
||||
const handleFieldSubmit = (data, currentField, editIndex) => {
|
||||
const isHeadingType = data.type.startsWith('heading');
|
||||
const isContentTypeOnly = data.type === 'paragraph' || isHeadingType;
|
||||
const effectiveMasterFile = masterFile || currentField?.masterFileToUpload;
|
||||
|
||||
if (currentField?.masterFileToUpload && onMasterFileUpload) {
|
||||
onMasterFileUpload(currentField.masterFileToUpload);
|
||||
}
|
||||
|
||||
// Un champ fichier nécessite un document source déjà uploadé.
|
||||
if (data.type === 'file' && !effectiveMasterFile) {
|
||||
setSaveMessage({
|
||||
type: 'error',
|
||||
text:
|
||||
'Veuillez d\'abord uploader le document du formulaire avant d\'ajouter un champ fichier.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const fieldData = {
|
||||
...data,
|
||||
@ -653,7 +676,7 @@ export default function FormTemplateBuilder({
|
||||
<h3 className="text-lg font-semibold mb-4">Aperçu du formulaire</h3>
|
||||
<div className="border-2 border-dashed border-gray-300 p-6 rounded">
|
||||
{formConfig.fields.length > 0 ? (
|
||||
<FormRenderer formConfig={formConfig} />
|
||||
<FormRenderer formConfig={formConfig} masterFile={masterFile} />
|
||||
) : (
|
||||
<p className="text-gray-500 italic text-center">
|
||||
Ajoutez des champs pour voir l'aperçu
|
||||
@ -668,6 +691,8 @@ export default function FormTemplateBuilder({
|
||||
isOpen={showAddFieldModal}
|
||||
onClose={() => setShowAddFieldModal(false)}
|
||||
onSubmit={handleFieldSubmit}
|
||||
hasMasterFile={Boolean(masterFile)}
|
||||
onMasterFileUpload={onMasterFileUpload}
|
||||
editingField={
|
||||
editingIndex >= 0
|
||||
? formConfig.fields[editingIndex]
|
||||
|
||||
@ -11,6 +11,8 @@ const SignatureField = ({
|
||||
backgroundColor = '#ffffff',
|
||||
penColor = '#000000',
|
||||
penWidth = 2,
|
||||
displayWidth = 400,
|
||||
displayHeight = 200,
|
||||
}) => {
|
||||
const canvasRef = useRef(null);
|
||||
const [isDrawing, setIsDrawing] = useState(false);
|
||||
@ -29,9 +31,6 @@ const SignatureField = ({
|
||||
|
||||
// Support High DPI / Retina displays
|
||||
const devicePixelRatio = window.devicePixelRatio || 1;
|
||||
const displayWidth = 400;
|
||||
const displayHeight = 200;
|
||||
|
||||
// Ajuster la taille physique du canvas pour la haute résolution
|
||||
canvas.width = displayWidth * devicePixelRatio;
|
||||
canvas.height = displayHeight * devicePixelRatio;
|
||||
@ -56,7 +55,7 @@ const SignatureField = ({
|
||||
context.lineCap = 'round';
|
||||
context.lineJoin = 'round';
|
||||
context.globalCompositeOperation = 'source-over';
|
||||
}, [backgroundColor, penColor, penWidth]);
|
||||
}, [backgroundColor, penColor, penWidth, displayWidth, displayHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
initializeCanvas();
|
||||
@ -226,11 +225,12 @@ const SignatureField = ({
|
||||
setCurrentPath('');
|
||||
}
|
||||
|
||||
// Notifier le parent du changement avec SVG
|
||||
// Notifier le parent du changement avec PNG pour garantir
|
||||
// la compatibilite de rendu cote backend/PDF.
|
||||
if (onChange) {
|
||||
const newPaths = [...svgPaths, currentPath].filter((p) => p.length > 0);
|
||||
const svgData = generateSVG(newPaths);
|
||||
onChange(svgData);
|
||||
const canvas = canvasRef.current;
|
||||
const pngData = canvas ? canvas.toDataURL('image/png') : '';
|
||||
onChange(pngData);
|
||||
}
|
||||
},
|
||||
[isDrawing, onChange, svgPaths, currentPath]
|
||||
@ -238,7 +238,7 @@ const SignatureField = ({
|
||||
|
||||
// Générer le SVG à partir des paths
|
||||
const generateSVG = (paths) => {
|
||||
const svgContent = `<svg width="400" height="200" xmlns="http://www.w3.org/2000/svg">
|
||||
const svgContent = `<svg width="${displayWidth}" height="${displayHeight}" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="100%" height="100%" fill="${backgroundColor}"/>
|
||||
${paths
|
||||
.map(
|
||||
@ -257,9 +257,6 @@ const SignatureField = ({
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
// Effacer en tenant compte des dimensions d'affichage
|
||||
const displayWidth = 400;
|
||||
const displayHeight = 200;
|
||||
|
||||
context.clearRect(0, 0, displayWidth, displayHeight);
|
||||
context.fillStyle = backgroundColor;
|
||||
context.fillRect(0, 0, displayWidth, displayHeight);
|
||||
@ -273,6 +270,14 @@ const SignatureField = ({
|
||||
}
|
||||
};
|
||||
|
||||
const hintText = readOnly
|
||||
? isEmpty
|
||||
? 'Aucune signature'
|
||||
: 'Signature'
|
||||
: isEmpty
|
||||
? 'Signez dans la zone ci-dessus'
|
||||
: 'Signature capturée';
|
||||
|
||||
return (
|
||||
<div className="signature-field">
|
||||
{label && (
|
||||
@ -282,7 +287,7 @@ const SignatureField = ({
|
||||
</label>
|
||||
)}
|
||||
|
||||
<div className="border border-gray-300 rounded-lg p-4 bg-gray-50">
|
||||
<div className="border border-gray-300 rounded-lg p-3 bg-gray-50">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className={`border border-gray-200 bg-white rounded touch-none ${
|
||||
@ -307,16 +312,8 @@ const SignatureField = ({
|
||||
onTouchEnd={readOnly ? undefined : stopDrawing}
|
||||
/>
|
||||
|
||||
<div className="flex justify-between items-center mt-3">
|
||||
<div className="text-xs text-gray-500">
|
||||
{readOnly
|
||||
? isEmpty
|
||||
? 'Aucune signature'
|
||||
: 'Signature'
|
||||
: isEmpty
|
||||
? 'Signez dans la zone ci-dessus'
|
||||
: 'Signature capturée'}
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-2">
|
||||
<div className="text-xs text-gray-500">{hintText}</div>
|
||||
|
||||
{!readOnly && (
|
||||
<div className="flex gap-2">
|
||||
|
||||
@ -34,6 +34,44 @@ export default function DynamicFormsList({
|
||||
const [formsValidation, setFormsValidation] = useState({});
|
||||
const fileInputRefs = React.useRef({});
|
||||
|
||||
const extractResponses = (data, maxDepth = 8) => {
|
||||
let current = data;
|
||||
for (let i = 0; i < maxDepth; i += 1) {
|
||||
if (!current || typeof current !== 'object') return {};
|
||||
if (current.responses && typeof current.responses === 'object') {
|
||||
current = current.responses;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!current || typeof current !== 'object') return {};
|
||||
|
||||
const cleaned = { ...current };
|
||||
delete cleaned.formId;
|
||||
delete cleaned.id;
|
||||
delete cleaned.templateId;
|
||||
delete cleaned.responses;
|
||||
return cleaned;
|
||||
};
|
||||
|
||||
const hasLocalCompletion = (templateId) => {
|
||||
if (formsValidation[templateId] === true) return true;
|
||||
|
||||
const localData = formsData[templateId];
|
||||
if (localData instanceof FormData) return true;
|
||||
if (localData && typeof localData === 'object') {
|
||||
return Object.keys(localData).length > 0;
|
||||
}
|
||||
|
||||
const savedResponses = existingResponses[templateId];
|
||||
return !!(
|
||||
savedResponses &&
|
||||
typeof savedResponses === 'object' &&
|
||||
Object.keys(savedResponses).length > 0
|
||||
);
|
||||
};
|
||||
|
||||
// Initialiser les données avec les réponses existantes
|
||||
useEffect(() => {
|
||||
// Initialisation complète de formsValidation et formsData pour chaque template
|
||||
@ -90,11 +128,7 @@ export default function DynamicFormsList({
|
||||
useEffect(() => {
|
||||
// Un document est considéré comme "validé" s'il est validé par l'école OU complété localement OU déjà existant
|
||||
const allFormsValid = schoolFileTemplates.every(
|
||||
(tpl) =>
|
||||
tpl.isValidated === true ||
|
||||
(formsData[tpl.id] && Object.keys(formsData[tpl.id]).length > 0) ||
|
||||
(existingResponses[tpl.id] &&
|
||||
Object.keys(existingResponses[tpl.id]).length > 0)
|
||||
(tpl) => tpl.isValidated === true || hasLocalCompletion(tpl.id)
|
||||
);
|
||||
|
||||
onValidationChange(allFormsValid);
|
||||
@ -113,10 +147,12 @@ export default function DynamicFormsList({
|
||||
try {
|
||||
logger.debug('Soumission du formulaire:', { templateId, formData });
|
||||
|
||||
const normalizedResponses = extractResponses(formData);
|
||||
|
||||
// Sauvegarder les données du formulaire
|
||||
setFormsData((prev) => ({
|
||||
...prev,
|
||||
[templateId]: formData,
|
||||
[templateId]: normalizedResponses,
|
||||
}));
|
||||
|
||||
// Marquer le formulaire comme complété
|
||||
@ -145,13 +181,7 @@ export default function DynamicFormsList({
|
||||
* Vérifie si un formulaire est complété
|
||||
*/
|
||||
const isFormCompleted = (templateId) => {
|
||||
return (
|
||||
formsValidation[templateId] === true ||
|
||||
(formsData[templateId] &&
|
||||
Object.keys(formsData[templateId]).length > 0) ||
|
||||
(existingResponses[templateId] &&
|
||||
Object.keys(existingResponses[templateId]).length > 0)
|
||||
);
|
||||
return hasLocalCompletion(templateId);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -229,13 +259,7 @@ export default function DynamicFormsList({
|
||||
{
|
||||
schoolFileTemplates.filter((tpl) => {
|
||||
// Validé ou complété localement
|
||||
return (
|
||||
tpl.isValidated === true ||
|
||||
(formsData[tpl.id] &&
|
||||
Object.keys(formsData[tpl.id]).length > 0) ||
|
||||
(existingResponses[tpl.id] &&
|
||||
Object.keys(existingResponses[tpl.id]).length > 0)
|
||||
);
|
||||
return tpl.isValidated === true || hasLocalCompletion(tpl.id);
|
||||
}).length
|
||||
}
|
||||
{' / '}
|
||||
@ -247,14 +271,10 @@ export default function DynamicFormsList({
|
||||
// Helper pour état
|
||||
const getState = (tpl) => {
|
||||
if (tpl.isValidated === true) return 0; // validé
|
||||
const isCompletedLocally = !!(
|
||||
(formsData[tpl.id] &&
|
||||
Object.keys(formsData[tpl.id]).length > 0) ||
|
||||
(existingResponses[tpl.id] &&
|
||||
Object.keys(existingResponses[tpl.id]).length > 0)
|
||||
);
|
||||
if (isCompletedLocally) return 1; // complété/en attente
|
||||
return 2; // à compléter/refusé
|
||||
const isCompletedLocally = hasLocalCompletion(tpl.id);
|
||||
if (isCompletedLocally) return 1; // complété (en attente de traitement)
|
||||
if (tpl.isValidated === false) return 2; // refusé
|
||||
return 3; // à compléter
|
||||
};
|
||||
const sortedTemplates = [...schoolFileTemplates].sort((a, b) => {
|
||||
return getState(a) - getState(b);
|
||||
@ -268,12 +288,7 @@ export default function DynamicFormsList({
|
||||
typeof tpl.isValidated === 'boolean'
|
||||
? tpl.isValidated
|
||||
: undefined;
|
||||
const isCompletedLocally = !!(
|
||||
(formsData[tpl.id] &&
|
||||
Object.keys(formsData[tpl.id]).length > 0) ||
|
||||
(existingResponses[tpl.id] &&
|
||||
Object.keys(existingResponses[tpl.id]).length > 0)
|
||||
);
|
||||
const isCompletedLocally = hasLocalCompletion(tpl.id);
|
||||
|
||||
// Statut d'affichage
|
||||
let statusLabel = '';
|
||||
@ -300,31 +315,17 @@ export default function DynamicFormsList({
|
||||
: textClass;
|
||||
canEdit = false;
|
||||
} else if (isValidated === false) {
|
||||
if (isCompletedLocally) {
|
||||
statusLabel = 'Complété';
|
||||
statusColor = 'orange';
|
||||
icon = <Hourglass className="w-5 h-5 text-orange-400" />;
|
||||
bgClass = isActive ? 'bg-orange-200' : 'bg-orange-50';
|
||||
borderClass = isActive
|
||||
? 'border border-orange-300'
|
||||
: 'border border-orange-200';
|
||||
textClass = isActive
|
||||
? 'text-orange-900 font-semibold'
|
||||
: 'text-orange-700';
|
||||
canEdit = true;
|
||||
} else {
|
||||
statusLabel = 'Refusé';
|
||||
statusColor = 'red';
|
||||
icon = <XCircle className="w-5 h-5 text-red-500" />;
|
||||
bgClass = isActive ? 'bg-red-200' : 'bg-red-50';
|
||||
borderClass = isActive
|
||||
? 'border border-red-300'
|
||||
: 'border border-red-200';
|
||||
textClass = isActive
|
||||
? 'text-red-900 font-semibold'
|
||||
: 'text-red-700';
|
||||
canEdit = true;
|
||||
}
|
||||
statusLabel = 'Refusé';
|
||||
statusColor = 'red';
|
||||
icon = <Hourglass className="w-5 h-5 text-red-500" />;
|
||||
bgClass = isActive ? 'bg-red-200' : 'bg-red-50';
|
||||
borderClass = isActive
|
||||
? 'border border-red-300'
|
||||
: 'border border-red-200';
|
||||
textClass = isActive
|
||||
? 'text-red-900 font-semibold'
|
||||
: 'text-red-700';
|
||||
canEdit = true;
|
||||
} else {
|
||||
if (isCompletedLocally) {
|
||||
statusLabel = 'Complété';
|
||||
@ -405,17 +406,17 @@ export default function DynamicFormsList({
|
||||
<span className="px-2 py-0.5 rounded bg-emerald-100 text-emerald-700 text-sm font-semibold">
|
||||
Validé
|
||||
</span>
|
||||
) : (formsData[currentTemplate.id] &&
|
||||
Object.keys(formsData[currentTemplate.id]).length > 0) ||
|
||||
(existingResponses[currentTemplate.id] &&
|
||||
Object.keys(existingResponses[currentTemplate.id]).length >
|
||||
0) ? (
|
||||
) : currentTemplate.isValidated === false ? (
|
||||
<span className="px-2 py-0.5 rounded bg-red-100 text-red-700 text-sm font-semibold">
|
||||
Refusé
|
||||
</span>
|
||||
) : hasLocalCompletion(currentTemplate.id) ? (
|
||||
<span className="px-2 py-0.5 rounded bg-orange-100 text-orange-700 text-sm font-semibold">
|
||||
Complété
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-2 py-0.5 rounded bg-red-100 text-red-700 text-sm font-semibold">
|
||||
Refusé
|
||||
<span className="px-2 py-0.5 rounded bg-gray-100 text-gray-700 text-sm font-semibold">
|
||||
En attente
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@ -430,14 +431,10 @@ export default function DynamicFormsList({
|
||||
// Trouver l'index du template courant dans la liste triée
|
||||
const getState = (tpl) => {
|
||||
if (tpl.isValidated === true) return 0;
|
||||
const isCompletedLocally = !!(
|
||||
(formsData[tpl.id] &&
|
||||
Object.keys(formsData[tpl.id]).length > 0) ||
|
||||
(existingResponses[tpl.id] &&
|
||||
Object.keys(existingResponses[tpl.id]).length > 0)
|
||||
);
|
||||
const isCompletedLocally = hasLocalCompletion(tpl.id);
|
||||
if (isCompletedLocally) return 1;
|
||||
return 2;
|
||||
if (tpl.isValidated === false) return 2;
|
||||
return 3;
|
||||
};
|
||||
const sortedTemplates = [...schoolFileTemplates].sort(
|
||||
(a, b) => getState(a) - getState(b)
|
||||
@ -469,8 +466,11 @@ export default function DynamicFormsList({
|
||||
submitLabel:
|
||||
currentTemplate.formTemplateData?.submitLabel || 'Valider',
|
||||
}}
|
||||
masterFile={
|
||||
currentTemplate.master_file_url || currentTemplate.file || null
|
||||
}
|
||||
initialValues={
|
||||
formsData[currentTemplate.id] ||
|
||||
extractResponses(formsData[currentTemplate.id]) ||
|
||||
existingResponses[currentTemplate.id] ||
|
||||
{}
|
||||
}
|
||||
|
||||
@ -285,6 +285,29 @@ export default function InscriptionFormShared({
|
||||
throw new Error(`Template avec l'ID ${templateId} non trouvé`);
|
||||
}
|
||||
|
||||
// Aplatit les structures de type { responses: { responses: {...} } }
|
||||
// pour ne conserver que les reponses de champs (ex: sign).
|
||||
const extractResponses = (data, maxDepth = 8) => {
|
||||
let current = data;
|
||||
for (let i = 0; i < maxDepth; i += 1) {
|
||||
if (!current || typeof current !== 'object') return {};
|
||||
if (current.responses && typeof current.responses === 'object') {
|
||||
current = current.responses;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!current || typeof current !== 'object') return {};
|
||||
const cleaned = { ...current };
|
||||
delete cleaned.formId;
|
||||
delete cleaned.id;
|
||||
delete cleaned.templateId;
|
||||
delete cleaned.responses;
|
||||
return cleaned;
|
||||
};
|
||||
|
||||
const normalizedResponses = extractResponses(formData);
|
||||
|
||||
// Construire la structure complète avec la configuration et les réponses
|
||||
const formTemplateData = {
|
||||
id: currentTemplate.id,
|
||||
@ -300,17 +323,17 @@ export default function InscriptionFormShared({
|
||||
).map((field) => ({
|
||||
...field,
|
||||
...(field.type === 'checkbox'
|
||||
? { checked: formData[field.id] || false }
|
||||
? { checked: normalizedResponses[field.id] || false }
|
||||
: {}),
|
||||
...(field.type === 'radio' ? { selected: formData[field.id] } : {}),
|
||||
...(field.type === 'text' ||
|
||||
field.type === 'textarea' ||
|
||||
field.type === 'email'
|
||||
? { value: formData[field.id] || '' }
|
||||
...(field.type === 'radio'
|
||||
? { selected: normalizedResponses[field.id] }
|
||||
: {}),
|
||||
...(field.id
|
||||
? { value: normalizedResponses[field.id] ?? field.value ?? '' }
|
||||
: {}),
|
||||
})),
|
||||
submitLabel: currentTemplate.formTemplateData?.submitLabel || 'Valider',
|
||||
responses: formData,
|
||||
responses: normalizedResponses,
|
||||
};
|
||||
|
||||
// Sauvegarder les réponses du formulaire via l'API RegistrationSchoolFileTemplate
|
||||
@ -326,7 +349,7 @@ export default function InscriptionFormShared({
|
||||
logger.debug("Réponse de l'API:", result);
|
||||
|
||||
// Prendre en compte la réponse du back pour mettre à jour les réponses locales
|
||||
let newResponses = formData;
|
||||
let newResponses = normalizedResponses;
|
||||
if (
|
||||
result &&
|
||||
result.data &&
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Edit, Trash2, FileText, Star, ChevronDown, Plus } from 'lucide-react';
|
||||
import Modal from '@/components/Modal';
|
||||
import FormTemplateBuilder from '@/components/Form/FormTemplateBuilder';
|
||||
import {
|
||||
// GET
|
||||
fetchRegistrationFileGroups,
|
||||
@ -32,6 +32,7 @@ import CheckBox from '@/components/Form/CheckBox';
|
||||
import Button from '@/components/Form/Button';
|
||||
import InputText from '@/components/Form/InputText';
|
||||
import { getSecureFileUrl } from '@/utils/fileUrl';
|
||||
import { FE_ADMIN_STRUCTURE_FORM_BUILDER_URL } from '@/utils/Url';
|
||||
|
||||
function getItemBgColor(type, selected, forceTheme = false) {
|
||||
// Colonne gauche : blanc si rien n'est sélectionné, emerald si sélectionné
|
||||
@ -200,7 +201,7 @@ export default function FilesGroupsManagement({
|
||||
const [parentFiles, setParentFileMasters] = useState([]);
|
||||
const [groups, setGroups] = useState([]);
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [fileToEdit, setFileToEdit] = useState(null);
|
||||
const [isGroupModalOpen, setIsGroupModalOpen] = useState(false);
|
||||
@ -226,10 +227,8 @@ export default function FilesGroupsManagement({
|
||||
const handleDocDropdownSelect = (type) => {
|
||||
setIsDocDropdownOpen(false);
|
||||
if (type === 'formulaire') {
|
||||
// Ouvre la modale unique en mode création
|
||||
setIsEditing(false);
|
||||
setFileToEdit(null);
|
||||
setIsModalOpen(true);
|
||||
const groupParam = selectedGroupId ? `?groupId=${selectedGroupId}` : '';
|
||||
router.push(`${FE_ADMIN_STRUCTURE_FORM_BUILDER_URL}${groupParam}`);
|
||||
} else if (type === 'formulaire_existant') {
|
||||
setIsFileUploadPopupOpen(true);
|
||||
setFileToEdit({});
|
||||
@ -329,28 +328,29 @@ export default function FilesGroupsManagement({
|
||||
};
|
||||
|
||||
const editTemplateMaster = (file) => {
|
||||
// Si le formulaire n'est pas personnalisé, ouvrir la popup de téléchargement
|
||||
if (
|
||||
!file.formMasterData ||
|
||||
!Array.isArray(file.formMasterData.fields) ||
|
||||
file.formMasterData.fields.length === 0
|
||||
) {
|
||||
setFileToEdit(file);
|
||||
setIsFileUploadPopupOpen(true);
|
||||
setIsEditing(true);
|
||||
const isDynamic =
|
||||
file.formMasterData &&
|
||||
Array.isArray(file.formMasterData.fields) &&
|
||||
file.formMasterData.fields.length > 0;
|
||||
|
||||
if (isDynamic) {
|
||||
router.push(`${FE_ADMIN_STRUCTURE_FORM_BUILDER_URL}?id=${file.id}`);
|
||||
} else {
|
||||
setIsEditing(true);
|
||||
setFileToEdit(file);
|
||||
setIsModalOpen(true);
|
||||
setIsEditing(true);
|
||||
setIsFileUploadPopupOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateSchoolFileMaster = ({
|
||||
name,
|
||||
group_ids,
|
||||
formMasterData,
|
||||
file,
|
||||
}) => {
|
||||
const handleCreateSchoolFileMaster = (
|
||||
{
|
||||
name,
|
||||
group_ids,
|
||||
formMasterData,
|
||||
file,
|
||||
},
|
||||
onCreated
|
||||
) => {
|
||||
// Toujours envoyer en FormData, même sans fichier
|
||||
const dataToSend = new FormData();
|
||||
const jsonData = {
|
||||
@ -379,12 +379,12 @@ export default function FilesGroupsManagement({
|
||||
createRegistrationSchoolFileMaster(dataToSend, csrfToken)
|
||||
.then((data) => {
|
||||
setSchoolFileMasters((prevFiles) => [...prevFiles, data]);
|
||||
setIsModalOpen(false);
|
||||
showNotification(
|
||||
`Le formulaire "${name}" a été créé avec succès.`,
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
if (onCreated) onCreated(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error creating form:', error);
|
||||
@ -460,7 +460,6 @@ export default function FilesGroupsManagement({
|
||||
setSchoolFileMasters((prevFichiers) =>
|
||||
prevFichiers.map((f) => (f.id === id ? data : f))
|
||||
);
|
||||
setIsModalOpen(false);
|
||||
showNotification(
|
||||
`Le formulaire "${name}" a été modifié avec succès.`,
|
||||
'success',
|
||||
@ -495,7 +494,6 @@ export default function FilesGroupsManagement({
|
||||
setSchoolFileMasters((prevFichiers) =>
|
||||
prevFichiers.map((f) => (f.id === id ? data : f))
|
||||
);
|
||||
setIsModalOpen(false);
|
||||
showNotification(
|
||||
`Le formulaire "${name}" a été modifié avec succès.`,
|
||||
'success',
|
||||
@ -888,13 +886,6 @@ export default function FilesGroupsManagement({
|
||||
return count;
|
||||
};
|
||||
|
||||
// Utilitaire pour ouvrir la modale FormTemplateBuilder (création ou édition)
|
||||
const openFormBuilderModal = (editing = false, initialData = null) => {
|
||||
setIsEditing(editing);
|
||||
setFileToEdit(initialData);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{/* Aide optionnelle */}
|
||||
@ -1094,37 +1085,6 @@ export default function FilesGroupsManagement({
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Modals pour création/édition d'un formulaire dynamique */}
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
setIsOpen={(isOpen) => {
|
||||
setIsModalOpen(isOpen);
|
||||
if (!isOpen) {
|
||||
setFileToEdit(null);
|
||||
setIsEditing(false);
|
||||
}
|
||||
}}
|
||||
title={
|
||||
isEditing
|
||||
? 'Modification du formulaire'
|
||||
: 'Créer un formulaire personnalisé'
|
||||
}
|
||||
>
|
||||
<div className="w-11/12 h-5/6 max-w-5xl max-h-[90vh] overflow-y-auto">
|
||||
<FormTemplateBuilder
|
||||
onSave={(data) => {
|
||||
(isEditing
|
||||
? handleEditSchoolFileMaster
|
||||
: handleCreateSchoolFileMaster)(data);
|
||||
setIsModalOpen(false);
|
||||
}}
|
||||
initialData={isEditing ? fileToEdit : undefined}
|
||||
groups={groups}
|
||||
isEditing={isEditing}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Popup pour création/édition d'un formulaire d'école déjà existant */}
|
||||
<Modal
|
||||
isOpen={isFileUploadPopupOpen}
|
||||
@ -1262,11 +1222,13 @@ export default function FilesGroupsManagement({
|
||||
!fileToEdit?.file
|
||||
)
|
||||
return;
|
||||
handleCreateSchoolFileMaster({
|
||||
name: fileToEdit.name,
|
||||
group_ids: fileToEdit.groups,
|
||||
file: fileToEdit.file,
|
||||
});
|
||||
handleCreateSchoolFileMaster(
|
||||
{
|
||||
name: fileToEdit.name,
|
||||
group_ids: fileToEdit.groups,
|
||||
file: fileToEdit.file,
|
||||
}
|
||||
);
|
||||
setIsFileUploadPopupOpen(false);
|
||||
setFileToEdit(null);
|
||||
}}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { logger } from '@/utils/logger';
|
||||
import { getToken } from 'next-auth/jwt';
|
||||
|
||||
const BACKEND_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
@ -27,7 +28,6 @@ export default async function handler(req, res) {
|
||||
const backendRes = await fetch(backendUrl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token.token}`,
|
||||
Connection: 'close',
|
||||
},
|
||||
});
|
||||
|
||||
@ -48,7 +48,8 @@ export default async function handler(req, res) {
|
||||
|
||||
const buffer = Buffer.from(await backendRes.arrayBuffer());
|
||||
return res.send(buffer);
|
||||
} catch {
|
||||
} catch (error) {
|
||||
logger.error('Download proxy error:', error);
|
||||
return res.status(500).json({ error: 'Erreur lors du téléchargement' });
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,6 +103,8 @@ export const FE_ADMIN_CLASSES_URL = '/admin/classes';
|
||||
export const FE_ADMIN_STRUCTURE_URL = '/admin/structure';
|
||||
export const FE_ADMIN_STRUCTURE_SCHOOLCLASS_MANAGEMENT_URL =
|
||||
'/admin/structure/SchoolClassManagement';
|
||||
export const FE_ADMIN_STRUCTURE_FORM_BUILDER_URL =
|
||||
'/admin/structure/FormBuilder';
|
||||
|
||||
//ADMIN/DIRECTORY URL
|
||||
export const FE_ADMIN_DIRECTORY_URL = '/admin/directory';
|
||||
|
||||
@ -10,6 +10,12 @@
|
||||
*/
|
||||
export const getSecureFileUrl = (filePath) => {
|
||||
if (!filePath) return null;
|
||||
if (typeof filePath !== 'string') return null;
|
||||
|
||||
// URL deja proxifiee: la reutiliser telle quelle.
|
||||
if (filePath.startsWith('/api/download?')) {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
// Si c'est une URL absolue, extraire le chemin /data/...
|
||||
if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
|
||||
|
||||
Reference in New Issue
Block a user