diff --git a/src/App.tsx b/src/App.tsx index 49e8a60..b6d1963 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,9 +9,11 @@ import TrustCenter from '@/pages/TrustCenter'; import ServiceDesk from '@/pages/ServiceDesk'; import DocumentVault from '@/pages/DocumentVault'; import AccountSettings from '@/pages/AccountSettings'; -import { InfrastructureEvolution } from '@/pages/InfrastructureEvolution'; -import Analytics from '@/pages/Analytics'; -import Backups from '@/pages/Backups'; +import { InfrastructureEvolution } from '@/pages/trustcenter/InfrastructureEvolution'; +import Analytics from '@/pages/trustcenter/Analytics'; +import Backups from '@/pages/trustcenter/Backups'; +import NewTicket from '@/pages/servicedesk/NewTicket'; +import TicketDetail from '@/pages/servicedesk/TicketDetail'; function App() { const [isAuthenticated, setIsAuthenticated] = useState(pb.authStore.isValid); @@ -59,6 +61,8 @@ function App() { } /> } /> } /> + } /> + } /> {/* Sécurité : Si l'URL n'existe pas, on redirige vers le dashboard */} } /> diff --git a/src/components/auth/LocalLoginForm.tsx b/src/components/auth/LocalLoginForm.tsx new file mode 100644 index 0000000..c2403b3 --- /dev/null +++ b/src/components/auth/LocalLoginForm.tsx @@ -0,0 +1,47 @@ +import React, { useState } from 'react'; +import { Button } from '@/components/ui/Button'; + +interface LocalLoginFormProps { + onSubmit: (email: string, pass: string) => void; + isLoading: boolean; +} + +export const LocalLoginForm: React.FC = ({ onSubmit, isLoading }) => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSubmit(email, password); + }; + + return ( +
+
+ +
+ setEmail(e.target.value)} + className="block w-full rounded-md border border-slate-300 px-3 py-2 shadow-sm focus:border-blue-900 focus:ring-blue-900 sm:text-sm" + /> +
+
+ +
+ +
+ setPassword(e.target.value)} + className="block w-full rounded-md border border-slate-300 px-3 py-2 shadow-sm focus:border-blue-900 focus:ring-blue-900 sm:text-sm" + /> +
+
+ + +
+ ); +}; \ No newline at end of file diff --git a/src/components/auth/MfaForm.tsx b/src/components/auth/MfaForm.tsx new file mode 100644 index 0000000..bd6bc2f --- /dev/null +++ b/src/components/auth/MfaForm.tsx @@ -0,0 +1,56 @@ +import React, { useState } from 'react'; +import { QRCodeSVG } from 'qrcode.react'; +import { Button } from '@/components/ui/Button'; + +interface MfaFormProps { + isFirstSetup: boolean; + qrUrl: string; + isLoading: boolean; + onVerify: (code: string) => void; + onCancel: () => void; +} + +export const MfaForm: React.FC = ({ isFirstSetup, qrUrl, isLoading, onVerify, onCancel }) => { + const [code, setCode] = useState(''); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onVerify(code); + }; + + return ( +
+ {isFirstSetup ? ( +
+

Configuration de la sécurité

+

Scannez ce QR Code avec Google Authenticator ou Authy.

+
+ +
+
+ ) : ( +
+

Validation à double facteur

+

Saisissez le code généré par votre application d'authentification.

+
+ )} + +
+ setCode(e.target.value.replace(/\D/g, ''))} // Force les chiffres + className="block w-full rounded-md border border-slate-300 py-3 text-center text-2xl tracking-[0.5em] text-slate-900 font-mono shadow-sm focus:border-blue-900 focus:ring-blue-900" + /> +
+ +
+ + +
+
+ ); +}; \ No newline at end of file diff --git a/src/components/ui/BackButton.tsx b/src/components/ui/BackButton.tsx new file mode 100644 index 0000000..e38d897 --- /dev/null +++ b/src/components/ui/BackButton.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Button, type ButtonProps } from '@/components/ui/Button'; + +export interface BackButtonProps extends Omit { + /** Route explicite vers laquelle naviguer (ex: "/support"). Si non spécifié, fait un retour arrière navigateur (-1) */ + to?: string; + /** Texte à afficher à côté de la flèche */ + label?: string; + /** Callback optionnel si vous souhaitez intercepter le clic */ + onClick?: (e: React.MouseEvent) => void; +} + +export const BackButton: React.FC = ({ + to, + label = "Retour", + onClick, + variant = "ghost", + size = "sm", + children, + className, + ...props +}) => { + const navigate = useNavigate(); + + const handleClick = (e: React.MouseEvent) => { + if (onClick) { + onClick(e); + } else if (to) { + navigate(to); + } else { + navigate(-1); // Comportement natif "Précédent" dans l'historique + } + }; + + return ( + + ); +}; + +export default BackButton; \ No newline at end of file diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index b659fab..bd371e9 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -1,37 +1,90 @@ -import React from 'react'; +import React, { forwardRef } from 'react'; import { cn } from '@/utils/utils'; -interface ButtonProps extends React.ButtonHTMLAttributes { - variant?: 'primary' | 'outline' | 'ghost'; - size?: 'sm' | 'md' | 'lg'; +// 1. Dictionnaire des variantes (Sémantique de couleurs GISE) +const variantStyles = { + primary: "bg-blue-900 text-white hover:bg-blue-800 focus:ring-blue-900 shadow-sm border border-transparent", + secondary: "bg-slate-900 text-white hover:bg-slate-800 focus:ring-slate-900 shadow-sm border border-transparent", + danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-600 shadow-sm border border-transparent", + success: "bg-emerald-600 text-white hover:bg-emerald-700 focus:ring-emerald-600 shadow-sm border border-transparent", + outline: "bg-white text-slate-700 border border-slate-300 hover:bg-slate-50 focus:ring-slate-900", + ghost: "bg-transparent text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus:ring-slate-900 border border-transparent", +}; + +// 2. Dictionnaire des tailles +const sizeStyles = { + sm: "px-3 py-1.5 text-xs", + md: "px-4 py-2.5 text-sm", + lg: "px-6 py-3 text-base w-full sm:w-auto", // w-full sur mobile par défaut, auto sur desktop + icon: "p-2", // Format carré optimisé pour les boutons sans texte +}; + +export interface ButtonProps extends React.ButtonHTMLAttributes { + variant?: keyof typeof variantStyles; + size?: keyof typeof sizeStyles; isLoading?: boolean; + leftIcon?: React.ReactNode; + rightIcon?: React.ReactNode; } -export const Button: React.FC = ({ - children, variant = 'primary', size = 'md', isLoading, className, disabled, ...props -}) => { - const baseStyles = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2"; - - const variants = { - primary: "bg-blue-900 text-white hover:bg-blue-800 focus:ring-blue-900 shadow-sm", - outline: "border border-slate-200 text-slate-900 hover:border-blue-900 hover:bg-blue-50 focus:ring-blue-900", - ghost: "text-slate-600 hover:text-slate-900 hover:bg-slate-100 focus:ring-slate-500", - }; +export const Button = forwardRef( + ( + { + className, + variant = 'primary', + size = 'md', + isLoading = false, + leftIcon, + rightIcon, + children, + disabled, + type = "button", + ...props + }, + ref + ) => { + const newLocal = "ml-2 flex-shrink-0"; + return ( + + ); + } +); - return ( - - ); -}; \ No newline at end of file +Button.displayName = "Button"; \ No newline at end of file diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx new file mode 100644 index 0000000..3602422 --- /dev/null +++ b/src/components/ui/Input.tsx @@ -0,0 +1,75 @@ +import React, { forwardRef, useId } from 'react'; +import { cn } from '@/utils/utils'; + +const sizeStyles = { + sm: "px-3 py-1.5 text-xs", + md: "px-4 py-2.5 text-sm", +}; + +export interface InputProps extends React.InputHTMLAttributes { + label?: string; + error?: string; + sizeVariant?: keyof typeof sizeStyles; // Gestion de la taille + leftIcon?: React.ReactNode; // Icône optionnelle intégrée à gauche + rightIcon?: React.ReactNode; // Icône optionnelle intégrée à droite +} + +export const Input = forwardRef( + ({ + className, + label, + error, + id, + sizeVariant = 'md', + leftIcon, + rightIcon, + ...props + }, ref) => { + const autoId = useId(); + const inputId = id || autoId; + + return ( +
+ {label && ( + + )} + +
+ {/* Icône à gauche */} + {leftIcon && ( + + {leftIcon} + + )} + + + + {/* Icône à droite */} + {rightIcon && ( + + {rightIcon} + + )} +
+ + {error &&

{error}

} +
+ ); + } +); + +Input.displayName = "Input"; \ No newline at end of file diff --git a/src/components/ui/MessageBubble.tsx b/src/components/ui/MessageBubble.tsx new file mode 100644 index 0000000..5959091 --- /dev/null +++ b/src/components/ui/MessageBubble.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { cn } from '@/utils/utils'; +import type { TicketMessage } from '@/types/Message'; + +interface MessageBubbleProps { + message: TicketMessage; + currentUserId?: string; +} + +export const MessageBubble: React.FC = ({ message, currentUserId }) => { + const isMyMessage = message.expand?.author?.id === currentUserId; + const authorName = message.expand?.author?.name || message.expand?.author?.email || 'Expert GISE'; + const time = new Date(message.created).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }); + + return ( +
+
+ {authorName} + {time} +
+ +
+ {message.content} +
+
+ ); +}; \ No newline at end of file diff --git a/src/components/ui/PageHeader.tsx b/src/components/ui/PageHeader.tsx new file mode 100644 index 0000000..6004165 --- /dev/null +++ b/src/components/ui/PageHeader.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { Card, CardContent } from '@/components/ui/Card'; + +interface PageHeaderProps { + title: string; + description: string; + /** Le contenu optionnel à afficher sur la droite (Bouton, Badge, etc.) */ + children?: React.ReactNode; +} + +export const PageHeader: React.FC = ({ title, description, children }) => { + return ( + + +
+

+ {title} +

+

+ {description} +

+
+ + {/* S'il y a des actions passées en "children", on les affiche ici */} + {children && ( +
+ {children} +
+ )} +
+
+ ); +}; + +export default PageHeader; \ No newline at end of file diff --git a/src/components/ui/Select.tsx b/src/components/ui/Select.tsx new file mode 100644 index 0000000..ed8318d --- /dev/null +++ b/src/components/ui/Select.tsx @@ -0,0 +1,55 @@ +import React, { forwardRef, useId } from 'react'; +import { cn } from '@/utils/utils'; + +export interface SelectOption { + value: string | number; + label: string; +} + +export interface SelectProps extends React.SelectHTMLAttributes { + label?: string; + error?: string; + options: SelectOption[]; + placeholder?: string; // Optionnel : ajoute un choix vide par défaut +} + +export const Select = forwardRef( + ({ className, label, error, id, options, placeholder, ...props }, ref) => { + const autoId = useId(); + const selectId = id || autoId; + + return ( +
+ {label && ( + + )} + + {error &&

{error}

} +
+ ); + } +); + +Select.displayName = "Select"; \ No newline at end of file diff --git a/src/components/ui/StatusBadge.tsx b/src/components/ui/StatusBadge.tsx index f1a7875..75eb2b0 100644 --- a/src/components/ui/StatusBadge.tsx +++ b/src/components/ui/StatusBadge.tsx @@ -20,26 +20,29 @@ const STATUS_CONFIG: Record = { bg: 'bg-emerald-50 text-emerald-700 border-emerald-200/60', dot: 'bg-emerald-500', }, + 'Ouvert': { bg: 'bg-blue-50 text-blue-700 border-blue-200/60', dot: 'bg-blue-500' }, // Statuts Intermédiaires / En cours 'Dégradé': { bg: 'bg-amber-50 text-amber-700 border-amber-200/60', dot: 'bg-amber-500', }, - 'En déploiement': { + 'En Déploiement': { bg: 'bg-blue-50 text-blue-700 border-blue-200/60', dot: 'bg-blue-500', }, + 'En Analyse': { bg: 'bg-amber-50 text-amber-700 border-amber-200/60', dot: 'bg-amber-500' }, // Statuts Alertes / Inactifs 'Hors Ligne': { bg: 'bg-red-50 text-red-700 border-red-200/60', dot: 'bg-red-500', }, - 'Suspendu': { + 'Annulé': { bg: 'bg-slate-100 text-slate-700 border-slate-200', dot: 'bg-slate-400', }, + 'Résolu': { bg: 'bg-emerald-50 text-emerald-700 border-emerald-200/60', dot: 'bg-emerald-500' }, }; export const StatusBadge: React.FC = ({ status, className }) => { diff --git a/src/components/ui/Textarea.tsx b/src/components/ui/Textarea.tsx new file mode 100644 index 0000000..96f2b51 --- /dev/null +++ b/src/components/ui/Textarea.tsx @@ -0,0 +1,38 @@ +import React, { forwardRef, useId } from 'react'; +import { cn } from '@/utils/utils'; + +export interface TextareaProps extends React.TextareaHTMLAttributes { + label?: string; + error?: string; +} + +export const Textarea = forwardRef( + ({ className, label, error, id, rows = 4, ...props }, ref) => { + const autoId = useId(); + const textareaId = id || autoId; + + return ( +
+ {label && ( + + )} +