Initial commit

This commit is contained in:
dahoud
2025-10-01 01:39:07 +00:00
commit b430bf3b96
826 changed files with 255287 additions and 0 deletions

View File

@@ -0,0 +1,885 @@
'use client';
import React, { useState, useRef, useEffect } from 'react';
import { Card } from 'primereact/card';
import { Button } from 'primereact/button';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { Toast } from 'primereact/toast';
import { Toolbar } from 'primereact/toolbar';
import { Tag } from 'primereact/tag';
import { Dialog } from 'primereact/dialog';
import { Calendar } from 'primereact/calendar';
import { Dropdown } from 'primereact/dropdown';
import { InputSwitch } from 'primereact/inputswitch';
import { InputNumber } from 'primereact/inputnumber';
import { ProgressBar } from 'primereact/progressbar';
import { FileUpload } from 'primereact/fileupload';
import { Checkbox } from 'primereact/checkbox';
import { RadioButton } from 'primereact/radiobutton';
import { Timeline } from 'primereact/timeline';
import { ConfirmDialog, confirmDialog } from 'primereact/confirmdialog';
import { Divider } from 'primereact/divider';
import { Panel } from 'primereact/panel';
import { Badge } from 'primereact/badge';
import { formatDate, formatDateTime } from '../../../../utils/formatters';
interface Backup {
id: string;
nom: string;
type: 'MANUEL' | 'AUTOMATIQUE' | 'PLANIFIE';
statut: 'EN_COURS' | 'COMPLETE' | 'ECHOUE' | 'PARTIEL';
dateCreation: Date;
taille: number;
duree: number;
fichiers: number;
destination: string;
chiffre: boolean;
compresse: boolean;
notes?: string;
utilisateur: string;
prochaineSauvegarde?: Date;
}
interface BackupSchedule {
id: string;
nom: string;
actif: boolean;
frequence: 'HORAIRE' | 'QUOTIDIEN' | 'HEBDOMADAIRE' | 'MENSUEL';
heure: Date;
joursSelection?: number[];
retention: number;
destination: string;
chiffrement: boolean;
compression: boolean;
inclureBase: boolean;
inclureFichiers: boolean;
inclureConfig: boolean;
}
interface RestorePoint {
id: string;
backupId: string;
nom: string;
date: Date;
type: string;
statut: 'DISPONIBLE' | 'CORROMPU' | 'EXPIRE';
taille: number;
}
const SauvegardePage = () => {
const [backups, setBackups] = useState<Backup[]>([]);
const [schedules, setSchedules] = useState<BackupSchedule[]>([]);
const [restorePoints, setRestorePoints] = useState<RestorePoint[]>([]);
const [loading, setLoading] = useState(false);
const [backupInProgress, setBackupInProgress] = useState(false);
const [progress, setProgress] = useState(0);
const [globalFilter, setGlobalFilter] = useState('');
const [scheduleDialog, setScheduleDialog] = useState(false);
const [restoreDialog, setRestoreDialog] = useState(false);
const [selectedBackup, setSelectedBackup] = useState<Backup | null>(null);
const [selectedSchedule, setSelectedSchedule] = useState<BackupSchedule | null>(null);
const [activeIndex, setActiveIndex] = useState(0);
const toast = useRef<Toast>(null);
const [newSchedule, setNewSchedule] = useState<BackupSchedule>({
id: '',
nom: '',
actif: true,
frequence: 'QUOTIDIEN',
heure: new Date(),
retention: 30,
destination: 'local',
chiffrement: true,
compression: true,
inclureBase: true,
inclureFichiers: true,
inclureConfig: true
});
const frequenceOptions = [
{ label: 'Toutes les heures', value: 'HORAIRE' },
{ label: 'Quotidien', value: 'QUOTIDIEN' },
{ label: 'Hebdomadaire', value: 'HEBDOMADAIRE' },
{ label: 'Mensuel', value: 'MENSUEL' }
];
const destinationOptions = [
{ label: 'Stockage local', value: 'local' },
{ label: 'Google Drive', value: 'google' },
{ label: 'Dropbox', value: 'dropbox' },
{ label: 'AWS S3', value: 's3' },
{ label: 'Serveur FTP', value: 'ftp' }
];
const joursSemaine = [
{ label: 'Lundi', value: 1 },
{ label: 'Mardi', value: 2 },
{ label: 'Mercredi', value: 3 },
{ label: 'Jeudi', value: 4 },
{ label: 'Vendredi', value: 5 },
{ label: 'Samedi', value: 6 },
{ label: 'Dimanche', value: 0 }
];
useEffect(() => {
loadBackupData();
}, []);
const loadBackupData = () => {
setLoading(true);
// TODO: Remplacer par un appel API réel pour charger l'historique des sauvegardes
// Exemple: const response = await fetch('/api/admin/backups');
// const backups = await response.json();
const mockBackups: Backup[] = [];
// TODO: Remplacer par un appel API réel pour charger les planifications de sauvegarde
// Exemple: const response = await fetch('/api/admin/backup-schedules');
// const schedules = await response.json();
const mockSchedules: BackupSchedule[] = [];
// TODO: Remplacer par un appel API réel pour charger les points de restauration
// Exemple: const response = await fetch('/api/admin/restore-points');
// const restorePoints = await response.json();
const mockRestorePoints: RestorePoint[] = [];
setTimeout(() => {
setBackups(mockBackups);
setSchedules(mockSchedules);
setRestorePoints(mockRestorePoints);
setLoading(false);
}, 1000);
};
const startManualBackup = () => {
confirmDialog({
message: 'Voulez-vous lancer une sauvegarde manuelle maintenant ?',
header: 'Confirmation de sauvegarde',
icon: 'pi pi-save',
acceptLabel: 'Lancer',
rejectLabel: 'Annuler',
accept: () => {
performBackup();
}
});
};
const performBackup = () => {
setBackupInProgress(true);
setProgress(0);
const interval = setInterval(() => {
setProgress(prev => {
if (prev >= 100) {
clearInterval(interval);
setBackupInProgress(false);
const newBackup: Backup = {
id: Date.now().toString(),
nom: `Sauvegarde manuelle ${new Date().toLocaleString()}`,
type: 'MANUEL',
statut: 'COMPLETE',
dateCreation: new Date(),
taille: Math.floor(Math.random() * 2000000000) + 500000000,
duree: Math.floor(Math.random() * 300) + 60,
fichiers: Math.floor(Math.random() * 10000) + 1000,
destination: 'local',
chiffre: true,
compresse: true,
utilisateur: 'admin'
};
setBackups([newBackup, ...backups]);
toast.current?.show({
severity: 'success',
summary: 'Sauvegarde terminée',
detail: 'La sauvegarde a été effectuée avec succès',
life: 5000
});
return 100;
}
return prev + 5;
});
}, 200);
};
const deleteBackup = (backup: Backup) => {
confirmDialog({
message: `Êtes-vous sûr de vouloir supprimer la sauvegarde "${backup.nom}" ?`,
header: 'Confirmation de suppression',
icon: 'pi pi-exclamation-triangle',
acceptLabel: 'Supprimer',
rejectLabel: 'Annuler',
acceptClassName: 'p-button-danger',
accept: () => {
setBackups(backups.filter(b => b.id !== backup.id));
toast.current?.show({
severity: 'success',
summary: 'Suppression réussie',
detail: 'La sauvegarde a été supprimée',
life: 3000
});
}
});
};
const restoreBackup = (backup: Backup) => {
setSelectedBackup(backup);
setRestoreDialog(true);
};
const performRestore = () => {
setRestoreDialog(false);
toast.current?.show({
severity: 'info',
summary: 'Restauration en cours',
detail: 'La restauration a été lancée en arrière-plan',
life: 5000
});
};
const saveSchedule = () => {
if (newSchedule.nom) {
const schedule = {
...newSchedule,
id: Date.now().toString()
};
setSchedules([...schedules, schedule]);
setScheduleDialog(false);
toast.current?.show({
severity: 'success',
summary: 'Planification créée',
detail: 'La planification de sauvegarde a été créée',
life: 3000
});
}
};
const toggleSchedule = (schedule: BackupSchedule) => {
const updated = schedules.map(s =>
s.id === schedule.id ? { ...s, actif: !s.actif } : s
);
setSchedules(updated);
toast.current?.show({
severity: schedule.actif ? 'warn' : 'success',
summary: schedule.actif ? 'Planification désactivée' : 'Planification activée',
detail: `La planification "${schedule.nom}" a été ${schedule.actif ? 'désactivée' : 'activée'}`,
life: 3000
});
};
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const formatDuration = (seconds: number) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (hours > 0) {
return `${hours}h ${minutes}m ${secs}s`;
} else if (minutes > 0) {
return `${minutes}m ${secs}s`;
}
return `${secs}s`;
};
const leftToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2">
<Button
label="Nouvelle sauvegarde"
icon="pi pi-save"
severity="success"
onClick={startManualBackup}
disabled={backupInProgress}
/>
<Button
label="Nouvelle planification"
icon="pi pi-calendar-plus"
onClick={() => {
setNewSchedule({
id: '',
nom: '',
actif: true,
frequence: 'QUOTIDIEN',
heure: new Date(),
retention: 30,
destination: 'local',
chiffrement: true,
compression: true,
inclureBase: true,
inclureFichiers: true,
inclureConfig: true
});
setScheduleDialog(true);
}}
/>
<Button
label="Importer"
icon="pi pi-upload"
severity="info"
/>
</div>
);
};
const rightToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2">
<Button
label="Nettoyer"
icon="pi pi-trash"
severity="warning"
tooltip="Supprimer les anciennes sauvegardes"
onClick={() => {
confirmDialog({
message: 'Supprimer toutes les sauvegardes de plus de 90 jours ?',
header: 'Nettoyage des sauvegardes',
icon: 'pi pi-exclamation-triangle',
acceptLabel: 'Nettoyer',
rejectLabel: 'Annuler',
acceptClassName: 'p-button-warning',
accept: () => {
toast.current?.show({
severity: 'success',
summary: 'Nettoyage effectué',
detail: '3 anciennes sauvegardes supprimées',
life: 3000
});
}
});
}}
/>
<Button
label="Actualiser"
icon="pi pi-refresh"
onClick={loadBackupData}
/>
</div>
);
};
const statusBodyTemplate = (rowData: Backup) => {
const getSeverity = (status: string) => {
switch (status) {
case 'COMPLETE': return 'success';
case 'EN_COURS': return 'info';
case 'PARTIEL': return 'warning';
case 'ECHOUE': return 'danger';
default: return 'secondary';
}
};
const getLabel = (status: string) => {
switch (status) {
case 'COMPLETE': return 'Complète';
case 'EN_COURS': return 'En cours';
case 'PARTIEL': return 'Partielle';
case 'ECHOUE': return 'Échouée';
default: return status;
}
};
return <Tag value={getLabel(rowData.statut)} severity={getSeverity(rowData.statut)} />;
};
const typeBodyTemplate = (rowData: Backup) => {
const getIcon = (type: string) => {
switch (type) {
case 'MANUEL': return 'pi-user';
case 'AUTOMATIQUE': return 'pi-clock';
case 'PLANIFIE': return 'pi-calendar';
default: return 'pi-save';
}
};
const getLabel = (type: string) => {
switch (type) {
case 'MANUEL': return 'Manuelle';
case 'AUTOMATIQUE': return 'Automatique';
case 'PLANIFIE': return 'Planifiée';
default: return type;
}
};
return (
<div className="flex align-items-center">
<i className={`pi ${getIcon(rowData.type)} mr-2`} />
{getLabel(rowData.type)}
</div>
);
};
const actionBodyTemplate = (rowData: Backup) => {
return (
<div className="flex gap-2">
<Button
icon="pi pi-download"
rounded
severity="info"
size="small"
tooltip="Télécharger"
/>
<Button
icon="pi pi-replay"
rounded
severity="success"
size="small"
tooltip="Restaurer"
onClick={() => restoreBackup(rowData)}
/>
<Button
icon="pi pi-trash"
rounded
severity="danger"
size="small"
tooltip="Supprimer"
onClick={() => deleteBackup(rowData)}
/>
</div>
);
};
const scheduleActionTemplate = (rowData: BackupSchedule) => {
return (
<div className="flex gap-2">
<InputSwitch
checked={rowData.actif}
onChange={() => toggleSchedule(rowData)}
/>
<Button
icon="pi pi-pencil"
rounded
severity="secondary"
size="small"
tooltip="Modifier"
/>
<Button
icon="pi pi-trash"
rounded
severity="danger"
size="small"
tooltip="Supprimer"
/>
</div>
);
};
const renderBackupList = () => (
<Card>
{backupInProgress && (
<div className="mb-4">
<h6>Sauvegarde en cours...</h6>
<ProgressBar value={progress} />
</div>
)}
<DataTable
value={backups}
paginator
rows={10}
dataKey="id"
loading={loading}
globalFilter={globalFilter}
emptyMessage="Aucune sauvegarde trouvée"
header={
<div className="flex justify-content-between align-items-center">
<h5 className="m-0">Historique des sauvegardes</h5>
<span className="p-input-icon-left">
<i className="pi pi-search" />
<InputText
placeholder="Rechercher..."
onInput={(e) => setGlobalFilter(e.currentTarget.value)}
/>
</span>
</div>
}
>
<Column field="nom" header="Nom" sortable />
<Column field="type" header="Type" body={typeBodyTemplate} sortable />
<Column field="statut" header="Statut" body={statusBodyTemplate} sortable />
<Column
field="dateCreation"
header="Date"
body={(rowData) => formatDateTime(rowData.dateCreation)}
sortable
/>
<Column
field="taille"
header="Taille"
body={(rowData) => formatFileSize(rowData.taille)}
sortable
/>
<Column
field="duree"
header="Durée"
body={(rowData) => formatDuration(rowData.duree)}
/>
<Column field="destination" header="Destination" />
<Column
field="chiffre"
header="Sécurité"
body={(rowData) => (
<div className="flex gap-1">
{rowData.chiffre && <Tag value="Chiffré" severity="success" />}
{rowData.compresse && <Tag value="Compressé" severity="info" />}
</div>
)}
/>
<Column body={actionBodyTemplate} headerStyle={{ width: '10rem' }} />
</DataTable>
</Card>
);
const renderSchedules = () => (
<Card>
<DataTable
value={schedules}
dataKey="id"
emptyMessage="Aucune planification trouvée"
header={<h5>Planifications de sauvegarde</h5>}
>
<Column field="nom" header="Nom" />
<Column
field="frequence"
header="Fréquence"
body={(rowData) => {
const freq = frequenceOptions.find(f => f.value === rowData.frequence);
return freq ? freq.label : rowData.frequence;
}}
/>
<Column
field="heure"
header="Heure"
body={(rowData) => rowData.heure.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
/>
<Column field="destination" header="Destination" />
<Column
field="retention"
header="Rétention"
body={(rowData) => `${rowData.retention} jours`}
/>
<Column
field="actif"
header="Statut"
body={(rowData) => (
<Tag
value={rowData.actif ? 'Active' : 'Inactive'}
severity={rowData.actif ? 'success' : 'secondary'}
/>
)}
/>
<Column body={scheduleActionTemplate} headerStyle={{ width: '12rem' }} />
</DataTable>
</Card>
);
const renderStatistics = () => (
<div className="grid">
<div className="col-12 md:col-3">
<Card>
<div className="text-center">
<i className="pi pi-database text-4xl text-primary mb-3" />
<div className="text-3xl font-bold text-primary">
{formatFileSize(backups.reduce((sum, b) => sum + b.taille, 0))}
</div>
<div className="text-color-secondary">Espace total utilisé</div>
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card>
<div className="text-center">
<i className="pi pi-save text-4xl text-green-500 mb-3" />
<div className="text-3xl font-bold text-green-500">
{backups.length}
</div>
<div className="text-color-secondary">Sauvegardes totales</div>
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card>
<div className="text-center">
<i className="pi pi-check-circle text-4xl text-blue-500 mb-3" />
<div className="text-3xl font-bold text-blue-500">
{backups.filter(b => b.statut === 'COMPLETE').length}
</div>
<div className="text-color-secondary">Sauvegardes réussies</div>
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card>
<div className="text-center">
<i className="pi pi-calendar text-4xl text-purple-500 mb-3" />
<div className="text-3xl font-bold text-purple-500">
{schedules.filter(s => s.actif).length}
</div>
<div className="text-color-secondary">Planifications actives</div>
</div>
</Card>
</div>
</div>
);
const scheduleDialogFooter = (
<div className="flex justify-content-end gap-2">
<Button label="Annuler" icon="pi pi-times" outlined onClick={() => setScheduleDialog(false)} />
<Button label="Créer" icon="pi pi-check" onClick={saveSchedule} />
</div>
);
const restoreDialogFooter = (
<div className="flex justify-content-end gap-2">
<Button label="Annuler" icon="pi pi-times" outlined onClick={() => setRestoreDialog(false)} />
<Button label="Restaurer" icon="pi pi-replay" severity="success" onClick={performRestore} />
</div>
);
return (
<div className="grid">
<div className="col-12">
<Card title="Gestion des sauvegardes">
<Toast ref={toast} />
<ConfirmDialog />
<Toolbar className="mb-4" left={leftToolbarTemplate} right={rightToolbarTemplate} />
{renderStatistics()}
<div className="mt-4">
{renderBackupList()}
</div>
<div className="mt-4">
{renderSchedules()}
</div>
{/* Dialog pour nouvelle planification */}
<Dialog
visible={scheduleDialog}
style={{ width: '600px' }}
header="Nouvelle planification de sauvegarde"
modal
footer={scheduleDialogFooter}
onHide={() => setScheduleDialog(false)}
>
<div className="formgrid grid">
<div className="field col-12">
<label htmlFor="nom">Nom de la planification</label>
<InputText
id="nom"
value={newSchedule.nom}
onChange={(e) => setNewSchedule({...newSchedule, nom: e.target.value})}
className="w-full"
placeholder="Ex: Sauvegarde quotidienne"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="frequence">Fréquence</label>
<Dropdown
id="frequence"
value={newSchedule.frequence}
options={frequenceOptions}
onChange={(e) => setNewSchedule({...newSchedule, frequence: e.value})}
className="w-full"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="heure">Heure d'exécution</label>
<Calendar
id="heure"
value={newSchedule.heure}
onChange={(e) => setNewSchedule({...newSchedule, heure: e.value || new Date()})}
timeOnly
hourFormat="24"
className="w-full"
/>
</div>
{newSchedule.frequence === 'HEBDOMADAIRE' && (
<div className="field col-12">
<label>Jours de la semaine</label>
<div className="flex flex-wrap gap-3">
{joursSemaine.map(jour => (
<div key={jour.value} className="flex align-items-center">
<Checkbox
inputId={`jour-${jour.value}`}
value={jour.value}
onChange={(e) => {
const jours = newSchedule.joursSelection || [];
if (e.checked) {
setNewSchedule({...newSchedule, joursSelection: [...jours, jour.value]});
} else {
setNewSchedule({...newSchedule, joursSelection: jours.filter(j => j !== jour.value)});
}
}}
checked={newSchedule.joursSelection?.includes(jour.value) || false}
/>
<label htmlFor={`jour-${jour.value}`} className="ml-2">{jour.label}</label>
</div>
))}
</div>
</div>
)}
<div className="field col-12 md:col-6">
<label htmlFor="destination">Destination</label>
<Dropdown
id="destination"
value={newSchedule.destination}
options={destinationOptions}
onChange={(e) => setNewSchedule({...newSchedule, destination: e.value})}
className="w-full"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="retention">Rétention (jours)</label>
<InputNumber
id="retention"
value={newSchedule.retention}
onValueChange={(e) => setNewSchedule({...newSchedule, retention: e.value || 30})}
min={1}
max={365}
className="w-full"
/>
</div>
<Divider />
<div className="field col-12">
<h6>Options de sauvegarde</h6>
</div>
<div className="field col-12 md:col-4">
<div className="flex align-items-center">
<Checkbox
inputId="chiffrement"
checked={newSchedule.chiffrement}
onChange={(e) => setNewSchedule({...newSchedule, chiffrement: e.checked || false})}
/>
<label htmlFor="chiffrement" className="ml-2">Chiffrement</label>
</div>
</div>
<div className="field col-12 md:col-4">
<div className="flex align-items-center">
<Checkbox
inputId="compression"
checked={newSchedule.compression}
onChange={(e) => setNewSchedule({...newSchedule, compression: e.checked || false})}
/>
<label htmlFor="compression" className="ml-2">Compression</label>
</div>
</div>
<div className="field col-12">
<h6>Éléments à sauvegarder</h6>
</div>
<div className="field col-12 md:col-4">
<div className="flex align-items-center">
<Checkbox
inputId="inclureBase"
checked={newSchedule.inclureBase}
onChange={(e) => setNewSchedule({...newSchedule, inclureBase: e.checked || false})}
/>
<label htmlFor="inclureBase" className="ml-2">Base de données</label>
</div>
</div>
<div className="field col-12 md:col-4">
<div className="flex align-items-center">
<Checkbox
inputId="inclureFichiers"
checked={newSchedule.inclureFichiers}
onChange={(e) => setNewSchedule({...newSchedule, inclureFichiers: e.checked || false})}
/>
<label htmlFor="inclureFichiers" className="ml-2">Fichiers</label>
</div>
</div>
<div className="field col-12 md:col-4">
<div className="flex align-items-center">
<Checkbox
inputId="inclureConfig"
checked={newSchedule.inclureConfig}
onChange={(e) => setNewSchedule({...newSchedule, inclureConfig: e.checked || false})}
/>
<label htmlFor="inclureConfig" className="ml-2">Configuration</label>
</div>
</div>
</div>
</Dialog>
{/* Dialog pour restauration */}
<Dialog
visible={restoreDialog}
style={{ width: '500px' }}
header="Restaurer une sauvegarde"
modal
footer={restoreDialogFooter}
onHide={() => setRestoreDialog(false)}
>
{selectedBackup && (
<div>
<div className="mb-3">
<strong>Sauvegarde:</strong> {selectedBackup.nom}
</div>
<div className="mb-3">
<strong>Date:</strong> {formatDateTime(selectedBackup.dateCreation)}
</div>
<div className="mb-3">
<strong>Taille:</strong> {formatFileSize(selectedBackup.taille)}
</div>
<Divider />
<div className="p-message p-message-warn mb-3">
<i className="pi pi-exclamation-triangle mr-2"></i>
<span>
La restauration remplacera toutes les données actuelles.
Cette action est irréversible.
</span>
</div>
<div>
<h6>Options de restauration</h6>
<div className="field-radiobutton mb-2">
<RadioButton inputId="complete" name="restoreType" value="complete" checked />
<label htmlFor="complete" className="ml-2">Restauration complète</label>
</div>
<div className="field-radiobutton mb-2">
<RadioButton inputId="selective" name="restoreType" value="selective" />
<label htmlFor="selective" className="ml-2">Restauration sélective</label>
</div>
</div>
</div>
)}
</Dialog>
</Card>
</div>
</div>
);
};
export default SauvegardePage;