function ProjectView({ projectId, onBack, user }) { const [project, setProject] = React.useState(null); const [loading, setLoading] = React.useState(true); const [sidebarOpen, setSidebarOpen] = React.useState(true); const [previewFile, setPreviewFile] = React.useState(null); const [addPhaseModal, setAddPhaseModal] = React.useState(false); const [newPhase, setNewPhase] = React.useState({ ext_id: '', name: '', full_name: '', color: '#6b7280' }); const [addStepModal, setAddStepModal] = React.useState(null); // phase_id or null const [newStep, setNewStep] = React.useState({ name: '', abbr: '', tooltip: '', responsible: '', deliverable: '' }); const loadProject = async () => { try { const data = await API.get(`/api/projects/${projectId}`); setProject(data); } catch (err) { Toasts.error(err.message); } finally { setLoading(false); } }; React.useEffect(() => { loadProject(); }, [projectId]); const handleUpdateStep = async (stepId, updates) => { await API.put(`/api/projects/${projectId}/steps/${stepId}`, updates); await loadProject(); }; const handleDeleteStep = async (stepId) => { await API.delete(`/api/projects/${projectId}/steps/${stepId}`); Toasts.success('Étape supprimée'); await loadProject(); }; const handleAddComment = async (stepId, text) => { await API.post(`/api/projects/${projectId}/steps/${stepId}/comments`, { text }); await loadProject(); }; const handleDeleteComment = async (commentId) => { await API.delete(`/api/projects/${projectId}/comments/${commentId}`); Toasts.success('Commentaire supprimé'); await loadProject(); }; const handleUpload = async (stepId, file, onProgress) => { await API.upload(`/api/projects/${projectId}/steps/${stepId}/attachments`, file, onProgress); await loadProject(); }; const handleDeleteAttachment = async (attId) => { if (!confirm('Supprimer cette pièce jointe ?')) return; await API.delete(`/api/projects/${projectId}/attachments/${attId}`); Toasts.success('Pièce jointe supprimée'); await loadProject(); }; const handleUpdatePhase = async (phaseId, updates) => { await API.put(`/api/projects/${projectId}/phases/${phaseId}`, updates); await loadProject(); }; const handleDeletePhase = async (phaseId) => { await API.delete(`/api/projects/${projectId}/phases/${phaseId}`); Toasts.success('Phase supprimée'); await loadProject(); }; const handleAddPhase = async () => { if (!newPhase.name.trim()) return; try { await API.post(`/api/projects/${projectId}/phases`, newPhase); setAddPhaseModal(false); setNewPhase({ ext_id: '', name: '', full_name: '', color: '#6b7280' }); Toasts.success('Phase ajoutée'); await loadProject(); } catch (err) { Toasts.error(err.message); } }; const handleAddStep = async (phaseId) => { setAddStepModal(phaseId); setNewStep({ name: '', abbr: '', tooltip: '', responsible: '', deliverable: '' }); }; const handleCreateStep = async () => { if (!newStep.name.trim()) return; try { await API.post(`/api/projects/${projectId}/phases/${addStepModal}/steps`, newStep); setAddStepModal(null); Toasts.success('Étape ajoutée'); await loadProject(); } catch (err) { Toasts.error(err.message); } }; const handleExportJson = async () => { try { const res = await API.fetch(`/api/projects/${projectId}/export/json`); const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${project.name}_export.json`; a.click(); URL.revokeObjectURL(url); Toasts.success('Export JSON téléchargé'); } catch (err) { Toasts.error(err.message); } }; const handleExportPdf = async () => { try { const res = await API.fetch(`/api/projects/${projectId}/export/pdf`); const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${project.name}_rapport.pdf`; a.click(); URL.revokeObjectURL(url); Toasts.success('Export PDF téléchargé'); } catch (err) { Toasts.error(err.message); } }; const handleExportArchive = async () => { try { const res = await API.fetch(`/api/projects/${projectId}/export/archive`); const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${project.name}_archive.zip`; a.click(); URL.revokeObjectURL(url); Toasts.success('Archive ZIP téléchargée'); } catch (err) { Toasts.error(err.message); } }; // Email export modal state const [emailModal, setEmailModal] = React.useState(null); // null or { format: 'pdf'|'both' } const [emailRecipients, setEmailRecipients] = React.useState({}); const openEmailModal = (format) => { // Pre-select current user const me = API.getUser(); const defaults = {}; if (me && me.email) defaults[me.email] = true; setEmailRecipients(defaults); setEmailModal({ format }); }; const toggleRecipient = (email) => { setEmailRecipients(prev => ({ ...prev, [email]: !prev[email] })); }; const handleSendEmail = async () => { const selected = Object.entries(emailRecipients).filter(([_, v]) => v).map(([k]) => k); if (selected.length === 0) { Toasts.error('Sélectionnez au moins un destinataire'); return; } try { const res = await API.post(`/api/projects/${projectId}/export/email`, { format: emailModal.format, recipients: selected }); Toasts.success(res.message || 'Export envoyé'); setEmailModal(null); } catch (err) { Toasts.error(err.message); } }; const scrollToPhase = (phaseId) => { const el = document.getElementById(`phase-${phaseId}`); if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); }; if (loading) { return React.createElement('div', { className: 'loading-page' }, React.createElement('span', { className: 'loading-spinner' }), 'Chargement du projet...' ); } if (!project) { return React.createElement('div', { className: 'loading-page' }, 'Projet introuvable'); } const phases = project.phases || []; const allSteps = phases.flatMap(p => p.steps || []); const totalSteps = allSteps.length; const doneSteps = allSteps.filter(s => s.status === 'done').length; const activeSteps = allSteps.filter(s => s.status === 'active').length; const blockedSteps = allSteps.filter(s => s.status === 'blocked').length; const progressPct = totalSteps > 0 ? Math.round((doneSteps / totalSteps) * 100) : 0; return React.createElement('div', { className: 'grafcet-layout' }, // Sidebar React.createElement('div', { className: `sidebar ${sidebarOpen ? '' : 'collapsed'}` }, sidebarOpen && React.createElement(React.Fragment, null, React.createElement('div', { style: { marginBottom: '20px' } }, React.createElement('h3', { style: { fontSize: '14px', fontWeight: '700', marginBottom: '8px', color: 'var(--text-bright)' } }, project.name), React.createElement('div', { style: { display: 'flex', gap: '6px', flexWrap: 'wrap', marginBottom: '12px' } }, React.createElement('span', { className: 'stat-badge stat-done' }, '\u2713 ', doneSteps), React.createElement('span', { className: 'stat-badge stat-active' }, '\u25CF ', activeSteps), React.createElement('span', { className: 'stat-badge stat-blocked' }, '! ', blockedSteps) ), React.createElement('div', { className: 'global-progress' }, React.createElement('div', { className: 'global-progress-bar' }, React.createElement('div', { className: 'global-progress-fill', style: { width: `${progressPct}%` } }) ), React.createElement('span', { className: 'global-progress-text' }, `${progressPct}%`) ) ), React.createElement(MiniGrafcet, { phases, onSelectPhase: scrollToPhase }), React.createElement('div', { style: { marginTop: '20px', display: 'flex', flexDirection: 'column', gap: '6px' } }, React.createElement('button', { className: 'btn btn-ghost btn-sm btn-full', onClick: handleExportPdf }, '\uD83D\uDCC4 Export PDF'), React.createElement('button', { className: 'btn btn-ghost btn-sm btn-full', onClick: handleExportJson }, '\uD83D\uDCE5 Export JSON'), React.createElement('button', { className: 'btn btn-ghost btn-sm btn-full', onClick: handleExportArchive }, '\uD83D\uDCE6 Export Archive ZIP'), React.createElement('div', { style: { borderTop: '1px solid var(--border)', paddingTop: '6px', marginTop: '2px' } }), React.createElement('button', { className: 'btn btn-ghost btn-sm btn-full', onClick: () => openEmailModal('pdf') }, '\u2709 Envoyer PDF par mail'), React.createElement('button', { className: 'btn btn-ghost btn-sm btn-full', onClick: () => openEmailModal('both') }, '\u2709 Envoyer PDF+ZIP par mail'), React.createElement('div', { style: { borderTop: '1px solid var(--border)', paddingTop: '6px', marginTop: '2px' } }), React.createElement('button', { className: 'btn btn-ghost btn-sm btn-full', onClick: () => setAddPhaseModal(true) }, '+ Ajouter Phase') ) ) ), // Toggle sidebar React.createElement('button', { className: 'sidebar-toggle', style: { left: sidebarOpen ? '260px' : '0' }, onClick: () => setSidebarOpen(!sidebarOpen) }, sidebarOpen ? '\u25C0' : '\u25B6'), // Main content React.createElement('div', { className: 'main-content' }, phases.length === 0 ? React.createElement('div', { style: { textAlign: 'center', padding: '60px', color: 'var(--text-muted)' } }, React.createElement('p', { style: { fontSize: '48px', marginBottom: '16px' } }, '\uD83D\uDEE0'), React.createElement('p', null, 'Aucune phase. Ajoutez votre première phase !'), React.createElement('button', { className: 'btn btn-primary', style: { marginTop: '16px' }, onClick: () => setAddPhaseModal(true) }, '+ Ajouter Phase') ) : phases.map((phase, i) => React.createElement(React.Fragment, { key: phase.id }, React.createElement(PhaseGrafcet, { phase, projectId, onUpdateStep: handleUpdateStep, onDeleteStep: handleDeleteStep, onAddStep: handleAddStep, onAddComment: handleAddComment, onDeleteComment: handleDeleteComment, onUpload: handleUpload, onDeleteAttachment: handleDeleteAttachment, onUpdatePhase: handleUpdatePhase, onDeletePhase: handleDeletePhase, onPreview: setPreviewFile }) ) ) ), // File preview modal previewFile && React.createElement('div', { className: 'modal-overlay', onClick: () => { URL.revokeObjectURL(previewFile.url); setPreviewFile(null); } }, React.createElement('div', { className: 'modal-content large', onClick: e => e.stopPropagation() }, React.createElement('div', { className: 'modal-header' }, React.createElement('h3', null, previewFile.name), React.createElement('button', { className: 'modal-close', onClick: () => { URL.revokeObjectURL(previewFile.url); setPreviewFile(null); } }, '\u2715') ), previewFile.type && previewFile.type.startsWith('image/') ? React.createElement('img', { src: previewFile.url, className: 'preview-image', alt: previewFile.name }) : previewFile.type && previewFile.type.includes('pdf') ? React.createElement('iframe', { src: previewFile.url, className: 'preview-pdf' }) : React.createElement('p', null, 'Prévisualisation non disponible. ', React.createElement('a', { href: previewFile.url, download: previewFile.name, style: { color: 'var(--accent)' } }, 'Télécharger')) ) ), // Add phase modal addPhaseModal && React.createElement('div', { className: 'modal-overlay', onClick: e => { if (e.target === e.currentTarget) setAddPhaseModal(false); } }, React.createElement('div', { className: 'modal-content' }, React.createElement('div', { className: 'modal-header' }, React.createElement('h3', null, 'Nouvelle Phase'), React.createElement('button', { className: 'modal-close', onClick: () => setAddPhaseModal(false) }, '\u2715') ), React.createElement('div', { className: 'new-project-form' }, React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '100px 1fr', gap: '12px' } }, React.createElement('div', { className: 'form-group' }, React.createElement('label', null, 'ID'), React.createElement('input', { type: 'text', value: newPhase.ext_id, placeholder: 'P7', onChange: e => setNewPhase({...newPhase, ext_id: e.target.value}) }) ), React.createElement('div', { className: 'form-group' }, React.createElement('label', null, 'Nom court'), React.createElement('input', { type: 'text', value: newPhase.name, placeholder: 'QUALITÉ', onChange: e => setNewPhase({...newPhase, name: e.target.value}), autoFocus: true }) ) ), React.createElement('div', { className: 'form-group' }, React.createElement('label', null, 'Nom complet'), React.createElement('input', { type: 'text', value: newPhase.full_name, placeholder: 'Contrôle Qualité', onChange: e => setNewPhase({...newPhase, full_name: e.target.value}) }) ), React.createElement('div', { className: 'form-group' }, React.createElement('label', null, 'Couleur'), React.createElement('input', { type: 'color', value: newPhase.color, onChange: e => setNewPhase({...newPhase, color: e.target.value}), style: { height: '40px' } }) ) ), React.createElement('div', { className: 'modal-actions' }, React.createElement('button', { className: 'btn btn-ghost', onClick: () => setAddPhaseModal(false) }, 'Annuler'), React.createElement('button', { className: 'btn btn-primary', disabled: !newPhase.name.trim(), onClick: handleAddPhase }, 'Ajouter') ) ) ), // Add step modal addStepModal && React.createElement('div', { className: 'modal-overlay', onClick: e => { if (e.target === e.currentTarget) setAddStepModal(null); } }, React.createElement('div', { className: 'modal-content' }, React.createElement('div', { className: 'modal-header' }, React.createElement('h3', null, 'Nouvelle Étape'), React.createElement('button', { className: 'modal-close', onClick: () => setAddStepModal(null) }, '\u2715') ), React.createElement('div', { className: 'new-project-form' }, React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 150px', gap: '12px' } }, React.createElement('div', { className: 'form-group' }, React.createElement('label', null, 'Nom'), React.createElement('input', { type: 'text', value: newStep.name, placeholder: 'Nom de l\'étape', onChange: e => setNewStep({...newStep, name: e.target.value}), autoFocus: true }) ), React.createElement('div', { className: 'form-group' }, React.createElement('label', null, 'Abréviation'), React.createElement('input', { type: 'text', value: newStep.abbr, placeholder: 'ABBR', onChange: e => setNewStep({...newStep, abbr: e.target.value}) }) ) ), React.createElement('div', { className: 'form-group' }, React.createElement('label', null, 'Description / Tooltip'), React.createElement('input', { type: 'text', value: newStep.tooltip, onChange: e => setNewStep({...newStep, tooltip: e.target.value}), placeholder: 'Description détaillée...' }) ), React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' } }, React.createElement('div', { className: 'form-group' }, React.createElement('label', null, 'Responsable'), React.createElement('input', { type: 'text', value: newStep.responsible, onChange: e => setNewStep({...newStep, responsible: e.target.value}), placeholder: 'Équipe / Personne' }) ), React.createElement('div', { className: 'form-group' }, React.createElement('label', null, 'Livrable'), React.createElement('input', { type: 'text', value: newStep.deliverable, onChange: e => setNewStep({...newStep, deliverable: e.target.value}), placeholder: 'Livrable attendu' }) ) ) ), React.createElement('div', { className: 'modal-actions' }, React.createElement('button', { className: 'btn btn-ghost', onClick: () => setAddStepModal(null) }, 'Annuler'), React.createElement('button', { className: 'btn btn-primary', disabled: !newStep.name.trim(), onClick: handleCreateStep }, 'Ajouter') ) ) ), // Email recipients modal emailModal && React.createElement('div', { className: 'modal-overlay', onClick: e => { if (e.target === e.currentTarget) setEmailModal(null); } }, React.createElement('div', { className: 'modal-content' }, React.createElement('div', { className: 'modal-header' }, React.createElement('h3', null, emailModal.format === 'both' ? 'Envoyer PDF + ZIP par mail' : 'Envoyer PDF par mail'), React.createElement('button', { className: 'modal-close', onClick: () => setEmailModal(null) }, '\u2715') ), React.createElement('p', { style: { fontSize: '13px', color: 'var(--text-muted)', marginBottom: '16px' } }, 'Sélectionnez les destinataires :' ), // Current user (() => { const me = API.getUser(); const meEmail = me && me.email; const members = (project.members || []).filter(m => m.email); const allRecipients = []; if (meEmail) allRecipients.push({ email: meEmail, name: me.display_name || me.username, isMe: true }); members.forEach(m => { if (m.email && (!meEmail || m.email !== meEmail)) { allRecipients.push({ email: m.email, name: m.display_name || m.username, isMe: false }); } }); if (allRecipients.length === 0) { return React.createElement('p', { style: { color: 'var(--red)', fontSize: '13px' } }, 'Aucun membre avec une adresse email.'); } return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px' } }, allRecipients.map(r => React.createElement('label', { key: r.email, style: { display: 'flex', alignItems: 'center', gap: '10px', padding: '10px 14px', background: emailRecipients[r.email] ? 'var(--bg-hover)' : 'var(--bg-light)', borderRadius: 'var(--radius-sm)', cursor: 'pointer', border: emailRecipients[r.email] ? '1px solid var(--accent)' : '1px solid var(--border)', transition: 'var(--transition)' } }, React.createElement('input', { type: 'checkbox', checked: !!emailRecipients[r.email], onChange: () => toggleRecipient(r.email), style: { accentColor: 'var(--accent)' } }), React.createElement('div', null, React.createElement('div', { style: { fontSize: '13px', fontWeight: '600', color: 'var(--text-bright)' } }, r.name, r.isMe ? ' (moi)' : ''), React.createElement('div', { style: { fontSize: '12px', color: 'var(--text-muted)' } }, r.email) ) ) ) ); })(), React.createElement('div', { className: 'modal-actions', style: { marginTop: '20px' } }, React.createElement('button', { className: 'btn btn-ghost', onClick: () => setEmailModal(null) }, 'Annuler'), React.createElement('button', { className: 'btn btn-primary', disabled: !Object.values(emailRecipients).some(v => v), onClick: handleSendEmail }, '\u2709 Envoyer') ) ) ) ); }