Complete family planning application with: - React frontend with TypeScript - Node.js/Express backend with TypeScript - Python ingestion service for document processing - Planning ingestion service with LLM integration - Shared UI components and type definitions - OAuth integration for calendar synchronization - Comprehensive documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1031 lines
32 KiB
TypeScript
1031 lines
32 KiB
TypeScript
import { useEffect, useState, useRef } from "react";
|
||
import { useNavigate, useParams } from "react-router-dom";
|
||
import styled from "styled-components";
|
||
import { useChildren } from "../state/ChildrenContext";
|
||
import { API_BASE_URL, uploadPlanning, uploadAvatar } from "../services/api-client";
|
||
|
||
const Container = styled.div`
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
`;
|
||
|
||
const Header = styled.header`
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 24px;
|
||
margin-bottom: 32px;
|
||
padding: 30px;
|
||
border-radius: 24px;
|
||
background: linear-gradient(135deg, rgba(29, 36, 66, 0.95) 0%, rgba(45, 27, 78, 0.95) 100%);
|
||
border: 1px solid rgba(126, 136, 180, 0.3);
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||
`;
|
||
|
||
const BackButton = styled.button`
|
||
padding: 12px 20px;
|
||
border-radius: 12px;
|
||
border: 1px solid rgba(126, 136, 180, 0.4);
|
||
background: rgba(16, 22, 52, 0.9);
|
||
color: #d9dcff;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
|
||
&:hover {
|
||
transform: translateY(-2px);
|
||
background: rgba(26, 32, 62, 0.9);
|
||
}
|
||
`;
|
||
|
||
const Avatar = styled.div<{ $imageUrl?: string; $color: string }>`
|
||
width: 120px;
|
||
height: 120px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: 700;
|
||
font-size: 2.5rem;
|
||
background: ${({ $imageUrl, $color }) => $imageUrl ? `url(${$imageUrl}) center/cover` : $color};
|
||
color: ${({ $imageUrl }) => ($imageUrl ? "transparent" : "#040411")};
|
||
border: 4px solid ${({ $color }) => $color};
|
||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
||
`;
|
||
|
||
const HeaderInfo = styled.div`
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
`;
|
||
|
||
const Title = styled.h1`
|
||
margin: 0;
|
||
font-size: 2rem;
|
||
font-weight: 700;
|
||
`;
|
||
|
||
const Meta = styled.div`
|
||
display: flex;
|
||
gap: 20px;
|
||
flex-wrap: wrap;
|
||
color: var(--text-muted);
|
||
font-size: 14px;
|
||
`;
|
||
|
||
const MetaItem = styled.div<{ $icon?: string }>`
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
|
||
&::before {
|
||
content: '${props => props.$icon || "•"}';
|
||
color: #66d9ff;
|
||
}
|
||
`;
|
||
|
||
const PronoteStatus = styled.div<{ $connected: boolean }>`
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 8px 16px;
|
||
border-radius: 20px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
background: ${props => props.$connected ? 'rgba(46, 213, 115, 0.2)' : 'rgba(255, 107, 107, 0.2)'};
|
||
color: ${props => props.$connected ? '#2ed573' : '#ff6b6b'};
|
||
border: 1px solid ${props => props.$connected ? 'rgba(46, 213, 115, 0.4)' : 'rgba(255, 107, 107, 0.4)'};
|
||
|
||
&::before {
|
||
content: '●';
|
||
font-size: 8px;
|
||
}
|
||
`;
|
||
|
||
const ActionButtons = styled.div`
|
||
display: flex;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
margin-top: 20px;
|
||
`;
|
||
|
||
const Button = styled.button`
|
||
padding: 12px 24px;
|
||
border-radius: 12px;
|
||
border: none;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 14px;
|
||
|
||
&:hover {
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
&:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
`;
|
||
|
||
const PrimaryButton = styled(Button)`
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: #ffffff;
|
||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||
|
||
&:hover {
|
||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
||
}
|
||
`;
|
||
|
||
const SecondaryButton = styled(Button)`
|
||
background: rgba(255, 255, 255, 0.1);
|
||
color: white;
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
|
||
&:hover {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
}
|
||
`;
|
||
|
||
const PronoteButton = styled(Button)`
|
||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||
color: white;
|
||
box-shadow: 0 4px 15px rgba(240, 147, 251, 0.4);
|
||
|
||
&:hover {
|
||
box-shadow: 0 6px 20px rgba(240, 147, 251, 0.6);
|
||
}
|
||
`;
|
||
|
||
const DangerButton = styled(Button)`
|
||
background: rgba(255, 107, 107, 0.2);
|
||
color: #ff6b6b;
|
||
border: 1px solid rgba(255, 107, 107, 0.4);
|
||
|
||
&:hover {
|
||
background: rgba(255, 107, 107, 0.3);
|
||
}
|
||
`;
|
||
|
||
const MainGrid = styled.div`
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||
gap: 20px;
|
||
margin-bottom: 20px;
|
||
`;
|
||
|
||
const Card = styled.section`
|
||
padding: 25px;
|
||
border-radius: 20px;
|
||
background: rgba(29, 36, 66, 0.92);
|
||
border: 1px solid rgba(126, 136, 180, 0.22);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
transition: all 0.3s ease;
|
||
|
||
&:hover {
|
||
transform: translateY(-5px);
|
||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
|
||
}
|
||
`;
|
||
|
||
const CardHeader = styled.div`
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 10px;
|
||
`;
|
||
|
||
const CardTitle = styled.h2<{ $icon?: string }>`
|
||
margin: 0;
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
|
||
&::before {
|
||
content: '${props => props.$icon || ""}';
|
||
font-size: 24px;
|
||
color: #66d9ff;
|
||
}
|
||
`;
|
||
|
||
const CardBadge = styled.span`
|
||
background: rgba(255, 255, 255, 0.2);
|
||
padding: 6px 12px;
|
||
border-radius: 20px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
`;
|
||
|
||
const Select = styled.select`
|
||
padding: 12px 14px;
|
||
border-radius: 12px;
|
||
border: 1px solid rgba(126, 136, 180, 0.28);
|
||
background: rgba(16, 22, 52, 0.9);
|
||
color: #ffffff;
|
||
cursor: pointer;
|
||
width: 100%;
|
||
font-size: 14px;
|
||
`;
|
||
|
||
const Label = styled.label`
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
`;
|
||
|
||
const HolidayList = styled.div`
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
`;
|
||
|
||
const HolidayItem = styled.div`
|
||
padding: 15px;
|
||
border-radius: 10px;
|
||
background: rgba(46, 213, 115, 0.1);
|
||
border-left: 3px solid #2ed573;
|
||
`;
|
||
|
||
const HolidayName = styled.div`
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
margin-bottom: 5px;
|
||
`;
|
||
|
||
const HolidayDate = styled.div`
|
||
font-size: 12px;
|
||
color: rgba(255, 255, 255, 0.7);
|
||
margin-bottom: 3px;
|
||
`;
|
||
|
||
const HolidayType = styled.div`
|
||
font-size: 11px;
|
||
color: rgba(255, 255, 255, 0.5);
|
||
font-style: italic;
|
||
`;
|
||
|
||
const PersonalNotes = styled.textarea`
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: 10px;
|
||
padding: 15px;
|
||
min-height: 150px;
|
||
color: white;
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
resize: vertical;
|
||
font-family: inherit;
|
||
|
||
&:focus {
|
||
outline: none;
|
||
border-color: #667eea;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
}
|
||
`;
|
||
|
||
const EmptyState = styled.div`
|
||
text-align: center;
|
||
padding: 30px;
|
||
color: rgba(255, 255, 255, 0.5);
|
||
font-size: 14px;
|
||
`;
|
||
|
||
const StatusMessage = styled.div`
|
||
padding: 12px;
|
||
border-radius: 12px;
|
||
background: rgba(12, 18, 42, 0.7);
|
||
border: 1px solid rgba(126, 136, 180, 0.24);
|
||
color: var(--text-muted);
|
||
text-align: center;
|
||
`;
|
||
|
||
const ErrorText = styled.div`
|
||
color: #ff7b8a;
|
||
font-size: 0.9rem;
|
||
`;
|
||
|
||
const SuccessText = styled.div`
|
||
color: #2ed573;
|
||
font-size: 0.9rem;
|
||
`;
|
||
|
||
// Modal components
|
||
const ModalOverlay = styled.div`
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
backdrop-filter: blur(5px);
|
||
`;
|
||
|
||
const ModalContent = styled.div<{ $width?: string }>`
|
||
background: linear-gradient(135deg, rgba(29, 36, 66, 0.98) 0%, rgba(45, 27, 78, 0.98) 100%);
|
||
border-radius: 24px;
|
||
padding: 40px;
|
||
max-width: ${({ $width }) => $width || '500px'};
|
||
width: 90%;
|
||
border: 1px solid rgba(126, 136, 180, 0.3);
|
||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||
max-height: 90vh;
|
||
overflow-y: auto;
|
||
`;
|
||
|
||
const ModalTitle = styled.h2`
|
||
margin: 0 0 24px 0;
|
||
font-size: 24px;
|
||
text-align: center;
|
||
`;
|
||
|
||
const Input = styled.input`
|
||
width: 100%;
|
||
padding: 14px 16px;
|
||
border-radius: 12px;
|
||
border: 1px solid rgba(126, 136, 180, 0.28);
|
||
background: rgba(16, 22, 52, 0.9);
|
||
color: #ffffff;
|
||
font-size: 14px;
|
||
margin-bottom: 16px;
|
||
|
||
&:focus {
|
||
outline: none;
|
||
border-color: #667eea;
|
||
}
|
||
|
||
&::placeholder {
|
||
color: rgba(255, 255, 255, 0.5);
|
||
}
|
||
`;
|
||
|
||
const FileInput = styled.input`
|
||
display: none;
|
||
`;
|
||
|
||
const FileUploadArea = styled.div<{ $isDragOver: boolean }>`
|
||
border: 2px dashed ${({ $isDragOver }) => $isDragOver ? '#667eea' : 'rgba(126, 136, 180, 0.4)'};
|
||
border-radius: 12px;
|
||
padding: 40px;
|
||
text-align: center;
|
||
background: ${({ $isDragOver }) => $isDragOver ? 'rgba(102, 126, 234, 0.1)' : 'rgba(255, 255, 255, 0.05)'};
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
margin-bottom: 16px;
|
||
|
||
&:hover {
|
||
background: rgba(255, 255, 255, 0.08);
|
||
border-color: #667eea;
|
||
}
|
||
`;
|
||
|
||
const FileUploadIcon = styled.div`
|
||
font-size: 48px;
|
||
margin-bottom: 12px;
|
||
`;
|
||
|
||
const FileUploadText = styled.div`
|
||
color: rgba(255, 255, 255, 0.8);
|
||
font-size: 14px;
|
||
margin-bottom: 8px;
|
||
`;
|
||
|
||
const FileUploadHint = styled.div`
|
||
color: rgba(255, 255, 255, 0.5);
|
||
font-size: 12px;
|
||
`;
|
||
|
||
const SelectedFile = styled.div`
|
||
background: rgba(102, 126, 234, 0.2);
|
||
padding: 12px 16px;
|
||
border-radius: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 16px;
|
||
`;
|
||
|
||
const FileName = styled.div`
|
||
color: #ffffff;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
`;
|
||
|
||
const RemoveFileButton = styled.button`
|
||
background: rgba(255, 107, 107, 0.3);
|
||
border: none;
|
||
color: #ff6b6b;
|
||
padding: 6px 12px;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
|
||
&:hover {
|
||
background: rgba(255, 107, 107, 0.5);
|
||
}
|
||
`;
|
||
|
||
const ModalButtons = styled.div`
|
||
display: flex;
|
||
gap: 12px;
|
||
margin-top: 24px;
|
||
`;
|
||
|
||
const ColorPicker = styled.input`
|
||
width: 60px;
|
||
height: 40px;
|
||
border-radius: 8px;
|
||
border: 1px solid rgba(126, 136, 180, 0.28);
|
||
background: transparent;
|
||
cursor: pointer;
|
||
`;
|
||
|
||
const REGION_LABELS: Record<string, string> = {
|
||
"zone-a": "Zone A (Besançon, Bordeaux, Clermont-Ferrand, Dijon, Grenoble, Limoges, Lyon, Poitiers)",
|
||
"zone-b": "Zone B (Aix-Marseille, Amiens, Caen, Lille, Nancy-Metz, Nantes, Nice, Orléans-Tours, Reims, Rennes, Rouen, Strasbourg)",
|
||
"zone-c": "Zone C (Créteil, Montpellier, Paris, Toulouse, Versailles)",
|
||
corse: "Corse",
|
||
monaco: "Monaco",
|
||
guadeloupe: "Guadeloupe",
|
||
guyane: "Guyane",
|
||
martinique: "Martinique",
|
||
reunion: "Réunion",
|
||
mayotte: "Mayotte"
|
||
};
|
||
|
||
interface Holiday {
|
||
id: string;
|
||
title: string;
|
||
startDate: string;
|
||
endDate: string;
|
||
description?: string;
|
||
}
|
||
|
||
interface Child {
|
||
id: string;
|
||
fullName: string;
|
||
colorHex: string;
|
||
email?: string;
|
||
notes?: string;
|
||
avatar?: { url: string };
|
||
schoolRegion?: string;
|
||
}
|
||
|
||
export const ChildDetailScreen = () => {
|
||
const { childId } = useParams<{ childId: string }>();
|
||
const navigate = useNavigate();
|
||
const { children, updateChild, deleteChild } = useChildren();
|
||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||
const editAvatarInputRef = useRef<HTMLInputElement>(null);
|
||
|
||
const [child, setChild] = useState<Child | null>(null);
|
||
const [selectedRegion, setSelectedRegion] = useState<string>("");
|
||
const [holidays, setHolidays] = useState<Holiday[]>([]);
|
||
const [loadingHolidays, setLoadingHolidays] = useState(false);
|
||
const [saving, setSaving] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [success, setSuccess] = useState<string | null>(null);
|
||
|
||
// États des modals
|
||
const [showImportModal, setShowImportModal] = useState(false);
|
||
const [showEditModal, setShowEditModal] = useState(false);
|
||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||
|
||
// États Import
|
||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||
const [isDragOver, setIsDragOver] = useState(false);
|
||
const [importing, setImporting] = useState(false);
|
||
const [importProgress, setImportProgress] = useState<string>("");
|
||
|
||
// États Edit
|
||
const [editName, setEditName] = useState("");
|
||
const [editEmail, setEditEmail] = useState("");
|
||
const [editColor, setEditColor] = useState("#667eea");
|
||
const [editAvatar, setEditAvatar] = useState<File | null>(null);
|
||
const [editAvatarPreview, setEditAvatarPreview] = useState<string | null>(null);
|
||
|
||
const [personalNotes, setPersonalNotes] = useState("");
|
||
|
||
useEffect(() => {
|
||
const foundChild = children.find((c: Child) => c.id === childId);
|
||
if (foundChild) {
|
||
setChild(foundChild);
|
||
setSelectedRegion(foundChild.schoolRegion ?? "");
|
||
setPersonalNotes(foundChild.notes || "");
|
||
}
|
||
}, [childId, children]);
|
||
|
||
useEffect(() => {
|
||
if (selectedRegion) {
|
||
loadHolidays(selectedRegion);
|
||
} else {
|
||
setHolidays([]);
|
||
}
|
||
}, [selectedRegion]);
|
||
|
||
const loadHolidays = async (region: string) => {
|
||
setLoadingHolidays(true);
|
||
setError(null);
|
||
try {
|
||
const response = await fetch(`${API_BASE_URL}/holidays?region=${region}&year=${new Date().getFullYear()}`);
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
setHolidays(data.holidays);
|
||
} else {
|
||
setError("Impossible de charger les congés");
|
||
}
|
||
} catch (err) {
|
||
setError("Erreur de connexion au serveur");
|
||
} finally {
|
||
setLoadingHolidays(false);
|
||
}
|
||
};
|
||
|
||
const handleSaveRegion = async () => {
|
||
if (!child || !childId) return;
|
||
setSaving(true);
|
||
setError(null);
|
||
try {
|
||
await updateChild(childId, {
|
||
schoolRegion: selectedRegion || undefined
|
||
});
|
||
setSuccess("Région mise à jour avec succès !");
|
||
setTimeout(() => setSuccess(null), 3000);
|
||
} catch (err) {
|
||
setError("Impossible de sauvegarder la région");
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const handleSaveNotes = async () => {
|
||
if (!child || !childId) return;
|
||
|
||
try {
|
||
await updateChild(childId, {
|
||
notes: personalNotes
|
||
});
|
||
setSuccess("Notes sauvegardées !");
|
||
setTimeout(() => setSuccess(null), 3000);
|
||
} catch (err) {
|
||
setError("Erreur lors de la sauvegarde");
|
||
}
|
||
};
|
||
|
||
// Import functionality
|
||
const handleOpenImport = () => {
|
||
setShowImportModal(true);
|
||
setSelectedFile(null);
|
||
setImportProgress("");
|
||
setError(null);
|
||
};
|
||
|
||
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||
const files = event.target.files;
|
||
if (files && files.length > 0) {
|
||
setSelectedFile(files[0]);
|
||
}
|
||
};
|
||
|
||
const handleDragOver = (event: React.DragEvent) => {
|
||
event.preventDefault();
|
||
setIsDragOver(true);
|
||
};
|
||
|
||
const handleDragLeave = () => {
|
||
setIsDragOver(false);
|
||
};
|
||
|
||
const handleDrop = (event: React.DragEvent) => {
|
||
event.preventDefault();
|
||
setIsDragOver(false);
|
||
const files = event.dataTransfer.files;
|
||
if (files && files.length > 0) {
|
||
setSelectedFile(files[0]);
|
||
}
|
||
};
|
||
|
||
const handleImport = async () => {
|
||
if (!selectedFile || !childId) return;
|
||
|
||
setImporting(true);
|
||
setError(null);
|
||
setImportProgress("Analyse du fichier en cours...");
|
||
|
||
try {
|
||
const result = await uploadPlanning(childId, selectedFile);
|
||
setImportProgress(`Import réussi ! ${result.schedule.activities?.length || 0} activités détectées.`);
|
||
|
||
setTimeout(() => {
|
||
setShowImportModal(false);
|
||
setSuccess("Fichier importé avec succès !");
|
||
setTimeout(() => setSuccess(null), 3000);
|
||
}, 2000);
|
||
} catch (err) {
|
||
setError("Erreur lors de l'import du fichier");
|
||
setImportProgress("");
|
||
} finally {
|
||
setImporting(false);
|
||
}
|
||
};
|
||
|
||
// Edit functionality
|
||
const handleOpenEdit = () => {
|
||
if (!child) return;
|
||
setEditName(child.fullName);
|
||
setEditEmail(child.email || "");
|
||
setEditColor(child.colorHex);
|
||
setEditAvatar(null);
|
||
setEditAvatarPreview(child.avatar?.url || null);
|
||
setShowEditModal(true);
|
||
setError(null);
|
||
};
|
||
|
||
const handleEditAvatarSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||
const files = event.target.files;
|
||
if (files && files.length > 0) {
|
||
const file = files[0];
|
||
setEditAvatar(file);
|
||
|
||
// Create preview
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => {
|
||
setEditAvatarPreview(e.target?.result as string);
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
};
|
||
|
||
const handleSaveEdit = async () => {
|
||
if (!child || !childId) return;
|
||
|
||
setSaving(true);
|
||
setError(null);
|
||
|
||
try {
|
||
let avatarData = undefined;
|
||
|
||
// Upload avatar if changed
|
||
if (editAvatar) {
|
||
const uploadResult = await uploadAvatar(editAvatar);
|
||
avatarData = { url: uploadResult.url };
|
||
}
|
||
|
||
// Update child
|
||
await updateChild(childId, {
|
||
fullName: editName,
|
||
email: editEmail || undefined,
|
||
colorHex: editColor,
|
||
...(avatarData && { avatar: avatarData })
|
||
});
|
||
|
||
setShowEditModal(false);
|
||
setSuccess("Profil mis à jour avec succès !");
|
||
setTimeout(() => setSuccess(null), 3000);
|
||
} catch (err) {
|
||
setError("Erreur lors de la modification du profil");
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
// Delete functionality
|
||
const handleOpenDelete = () => {
|
||
setShowDeleteModal(true);
|
||
setError(null);
|
||
};
|
||
|
||
const handleConfirmDelete = async () => {
|
||
if (!childId) return;
|
||
|
||
setSaving(true);
|
||
setError(null);
|
||
|
||
try {
|
||
await deleteChild(childId);
|
||
navigate("/profiles");
|
||
} catch (err) {
|
||
setError("Erreur lors de la suppression du profil");
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const formatDate = (dateStr: string) => {
|
||
if (!dateStr) return '';
|
||
return new Date(dateStr).toLocaleDateString("fr-FR", {
|
||
day: "numeric",
|
||
month: "long",
|
||
year: "numeric"
|
||
});
|
||
};
|
||
|
||
if (!child) {
|
||
return <Container><StatusMessage>Chargement du profil...</StatusMessage></Container>;
|
||
}
|
||
|
||
const initials = child.fullName
|
||
.split(" ")
|
||
.filter(Boolean)
|
||
.map((part) => part.charAt(0))
|
||
.join("")
|
||
.slice(0, 2)
|
||
.toUpperCase();
|
||
|
||
return (
|
||
<Container>
|
||
<Header>
|
||
<BackButton onClick={() => navigate("/profiles")}>← Retour</BackButton>
|
||
<Avatar $color={child.colorHex} $imageUrl={child.avatar?.url}>
|
||
{!child.avatar?.url && initials}
|
||
</Avatar>
|
||
<HeaderInfo>
|
||
<Title>{child.fullName}</Title>
|
||
<Meta>
|
||
<MetaItem $icon="📍">
|
||
{selectedRegion ? REGION_LABELS[selectedRegion]?.split('(')[0] : "Zone non définie"}
|
||
</MetaItem>
|
||
{child.email && <MetaItem $icon="📧">{child.email}</MetaItem>}
|
||
</Meta>
|
||
<ActionButtons>
|
||
<PrimaryButton onClick={() => navigate(`/child/${childId}/planning`)}>
|
||
<span>📅</span>
|
||
Planning
|
||
</PrimaryButton>
|
||
<SecondaryButton onClick={handleOpenImport}>
|
||
<span>📥</span>
|
||
Importer
|
||
</SecondaryButton>
|
||
<SecondaryButton onClick={handleOpenEdit}>
|
||
<span>✏️</span>
|
||
Modifier
|
||
</SecondaryButton>
|
||
<DangerButton onClick={handleOpenDelete}>
|
||
<span>🗑️</span>
|
||
Supprimer
|
||
</DangerButton>
|
||
</ActionButtons>
|
||
</HeaderInfo>
|
||
</Header>
|
||
|
||
{error && <ErrorText style={{ marginBottom: '20px', textAlign: 'center' }}>{error}</ErrorText>}
|
||
{success && <SuccessText style={{ marginBottom: '20px', textAlign: 'center' }}>{success}</SuccessText>}
|
||
|
||
<MainGrid>
|
||
{/* Région scolaire et congés */}
|
||
<Card>
|
||
<CardTitle $icon="🏖️">Congés scolaires</CardTitle>
|
||
<Label>
|
||
Zone scolaire
|
||
<Select
|
||
value={selectedRegion}
|
||
onChange={(e) => setSelectedRegion(e.target.value)}
|
||
>
|
||
<option value="">-- Aucune zone sélectionnée --</option>
|
||
{Object.entries(REGION_LABELS).map(([value, label]) => (
|
||
<option key={value} value={value}>{label}</option>
|
||
))}
|
||
</Select>
|
||
</Label>
|
||
<SecondaryButton onClick={handleSaveRegion} disabled={saving || !selectedRegion}>
|
||
{saving ? "Enregistrement..." : "Enregistrer la région"}
|
||
</SecondaryButton>
|
||
{loadingHolidays ? (
|
||
<StatusMessage>Chargement des congés...</StatusMessage>
|
||
) : holidays.length > 0 ? (
|
||
<HolidayList>
|
||
{holidays.slice(0, 5).map((holiday) => (
|
||
<HolidayItem key={holiday.id}>
|
||
<HolidayName>{holiday.title}</HolidayName>
|
||
<HolidayDate>
|
||
{holiday.startDate === holiday.endDate
|
||
? formatDate(holiday.startDate)
|
||
: `Du ${formatDate(holiday.startDate)} au ${formatDate(holiday.endDate)}`}
|
||
</HolidayDate>
|
||
{holiday.description && <HolidayType>{holiday.description}</HolidayType>}
|
||
</HolidayItem>
|
||
))}
|
||
</HolidayList>
|
||
) : selectedRegion && (
|
||
<EmptyState>Aucun congé trouvé</EmptyState>
|
||
)}
|
||
</Card>
|
||
|
||
{/* Notes personnelles */}
|
||
<Card>
|
||
<CardTitle $icon="📝">Notes personnelles</CardTitle>
|
||
<PersonalNotes
|
||
value={personalNotes}
|
||
onChange={(e) => setPersonalNotes(e.target.value)}
|
||
placeholder="Ajoutez vos notes personnelles ici..."
|
||
/>
|
||
<PrimaryButton onClick={handleSaveNotes}>
|
||
<span>💾</span>
|
||
Enregistrer les notes
|
||
</PrimaryButton>
|
||
</Card>
|
||
</MainGrid>
|
||
|
||
{/* Modal Import */}
|
||
{showImportModal && (
|
||
<ModalOverlay onClick={() => !importing && setShowImportModal(false)}>
|
||
<ModalContent $width="600px" onClick={(e) => e.stopPropagation()}>
|
||
<ModalTitle>Importer un planning</ModalTitle>
|
||
<p style={{ textAlign: 'center', color: 'rgba(255,255,255,0.7)', marginBottom: '20px' }}>
|
||
Importez un fichier Excel, PDF, Image (JPG/PNG) ou JSON contenant le planning.
|
||
</p>
|
||
|
||
{!selectedFile ? (
|
||
<FileUploadArea
|
||
$isDragOver={isDragOver}
|
||
onDragOver={handleDragOver}
|
||
onDragLeave={handleDragLeave}
|
||
onDrop={handleDrop}
|
||
onClick={() => fileInputRef.current?.click()}
|
||
>
|
||
<FileUploadIcon>📁</FileUploadIcon>
|
||
<FileUploadText>
|
||
Glissez-déposez un fichier ici ou cliquez pour parcourir
|
||
</FileUploadText>
|
||
<FileUploadHint>
|
||
Formats acceptés : Excel (.xlsx, .xls), PDF, Images (.jpg, .png), JSON
|
||
</FileUploadHint>
|
||
</FileUploadArea>
|
||
) : (
|
||
<SelectedFile>
|
||
<FileName>📄 {selectedFile.name}</FileName>
|
||
<RemoveFileButton onClick={() => setSelectedFile(null)}>
|
||
Retirer
|
||
</RemoveFileButton>
|
||
</SelectedFile>
|
||
)}
|
||
|
||
<FileInput
|
||
ref={fileInputRef}
|
||
type="file"
|
||
accept=".xlsx,.xls,.pdf,.jpg,.jpeg,.png,.json"
|
||
onChange={handleFileSelect}
|
||
/>
|
||
|
||
{importProgress && (
|
||
<StatusMessage style={{ marginBottom: '16px' }}>
|
||
{importProgress}
|
||
</StatusMessage>
|
||
)}
|
||
|
||
{error && <ErrorText style={{ marginBottom: '16px' }}>{error}</ErrorText>}
|
||
|
||
<ModalButtons>
|
||
<SecondaryButton
|
||
onClick={() => setShowImportModal(false)}
|
||
disabled={importing}
|
||
style={{ flex: 1 }}
|
||
>
|
||
Annuler
|
||
</SecondaryButton>
|
||
<PrimaryButton
|
||
onClick={handleImport}
|
||
disabled={!selectedFile || importing}
|
||
style={{ flex: 1 }}
|
||
>
|
||
{importing ? "Import en cours..." : "Importer"}
|
||
</PrimaryButton>
|
||
</ModalButtons>
|
||
</ModalContent>
|
||
</ModalOverlay>
|
||
)}
|
||
|
||
{/* Modal Edit */}
|
||
{showEditModal && (
|
||
<ModalOverlay onClick={() => !saving && setShowEditModal(false)}>
|
||
<ModalContent onClick={(e) => e.stopPropagation()}>
|
||
<ModalTitle>Modifier le profil</ModalTitle>
|
||
|
||
<Label>
|
||
Nom complet
|
||
<Input
|
||
type="text"
|
||
value={editName}
|
||
onChange={(e) => setEditName(e.target.value)}
|
||
placeholder="Nom complet"
|
||
/>
|
||
</Label>
|
||
|
||
<Label>
|
||
Email (optionnel)
|
||
<Input
|
||
type="email"
|
||
value={editEmail}
|
||
onChange={(e) => setEditEmail(e.target.value)}
|
||
placeholder="email@exemple.com"
|
||
/>
|
||
</Label>
|
||
|
||
<Label>
|
||
Couleur
|
||
<ColorPicker
|
||
type="color"
|
||
value={editColor}
|
||
onChange={(e) => setEditColor(e.target.value)}
|
||
/>
|
||
</Label>
|
||
|
||
<Label>
|
||
Avatar (optionnel)
|
||
<SecondaryButton onClick={() => editAvatarInputRef.current?.click()} type="button">
|
||
<span>🖼️</span>
|
||
Choisir un avatar
|
||
</SecondaryButton>
|
||
<FileInput
|
||
ref={editAvatarInputRef}
|
||
type="file"
|
||
accept="image/*"
|
||
onChange={handleEditAvatarSelect}
|
||
/>
|
||
{editAvatarPreview && (
|
||
<div style={{ textAlign: 'center', marginTop: '10px' }}>
|
||
<img
|
||
src={editAvatarPreview}
|
||
alt="Preview"
|
||
style={{ width: '80px', height: '80px', borderRadius: '50%', objectFit: 'cover' }}
|
||
/>
|
||
</div>
|
||
)}
|
||
</Label>
|
||
|
||
{error && <ErrorText>{error}</ErrorText>}
|
||
|
||
<ModalButtons>
|
||
<SecondaryButton
|
||
onClick={() => setShowEditModal(false)}
|
||
disabled={saving}
|
||
style={{ flex: 1 }}
|
||
>
|
||
Annuler
|
||
</SecondaryButton>
|
||
<PrimaryButton
|
||
onClick={handleSaveEdit}
|
||
disabled={saving || !editName}
|
||
style={{ flex: 1 }}
|
||
>
|
||
{saving ? "Enregistrement..." : "Enregistrer"}
|
||
</PrimaryButton>
|
||
</ModalButtons>
|
||
</ModalContent>
|
||
</ModalOverlay>
|
||
)}
|
||
|
||
{/* Modal Delete */}
|
||
{showDeleteModal && (
|
||
<ModalOverlay onClick={() => !saving && setShowDeleteModal(false)}>
|
||
<ModalContent onClick={(e) => e.stopPropagation()}>
|
||
<ModalTitle>Supprimer le profil</ModalTitle>
|
||
<p style={{ textAlign: 'center', color: 'rgba(255,255,255,0.8)', marginBottom: '20px' }}>
|
||
Êtes-vous sûr de vouloir supprimer le profil de <strong>{child.fullName}</strong> ?
|
||
</p>
|
||
<p style={{ textAlign: 'center', color: '#ff6b6b', fontSize: '14px', marginBottom: '24px' }}>
|
||
Cette action déplacera le profil dans les archives. Vous pourrez le restaurer depuis les paramètres.
|
||
</p>
|
||
|
||
{error && <ErrorText style={{ marginBottom: '16px' }}>{error}</ErrorText>}
|
||
|
||
<ModalButtons>
|
||
<SecondaryButton
|
||
onClick={() => setShowDeleteModal(false)}
|
||
disabled={saving}
|
||
style={{ flex: 1 }}
|
||
>
|
||
Annuler
|
||
</SecondaryButton>
|
||
<DangerButton
|
||
onClick={handleConfirmDelete}
|
||
disabled={saving}
|
||
style={{ flex: 1 }}
|
||
>
|
||
{saving ? "Suppression..." : "Supprimer"}
|
||
</DangerButton>
|
||
</ModalButtons>
|
||
</ModalContent>
|
||
</ModalOverlay>
|
||
)}
|
||
</Container>
|
||
);
|
||
};
|