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);
|
||||
|
||||
Reference in New Issue
Block a user