const STATUTS = { wait: { label: 'En attente', icon: '\u25CB', color: 'var(--text-muted)' }, active: { label: 'En cours', icon: '\u25CF', color: 'var(--accent)' }, done: { label: 'Terminé', icon: '\u2713', color: 'var(--green)' }, blocked: { label: 'Bloqué', icon: '!', color: 'var(--red)' }, quote: { label: 'Chiffrage', icon: '\u20AC', color: 'var(--yellow)' }, validation: { label: 'Valid. client', icon: '?', color: 'var(--purple)' } }; function formatSize(bytes) { if (bytes < 1024) return bytes + ' o'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' Ko'; return (bytes / (1024 * 1024)).toFixed(1) + ' Mo'; } function GrafcetTransition({ color }) { return React.createElement('div', { className: 'grafcet-transition', style: { backgroundColor: color || 'var(--border)' } }); } function AttachmentItem({ att, projectId, onDelete, onPreview }) { const isImage = att.mime_type && att.mime_type.startsWith('image/'); const isPdf = att.mime_type && att.mime_type.includes('pdf'); const icon = isImage ? '\uD83D\uDDBC' : isPdf ? '\uD83D\uDCC4' : '\uD83D\uDCCE'; const handleDownload = async () => { try { const res = await API.fetch(`/api/attachments/${att.id}/download`); const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = att.name; a.click(); URL.revokeObjectURL(url); } catch (err) { Toasts.error('Erreur de téléchargement'); } }; const handlePreview = async () => { try { const res = await API.fetch(`/api/attachments/${att.id}/download`); const blob = await res.blob(); const url = URL.createObjectURL(blob); onPreview({ name: att.name, url, type: att.mime_type }); } catch (err) { Toasts.error('Erreur de prévisualisation'); } }; return React.createElement('div', { className: 'attachment-item' }, React.createElement('span', { className: 'attachment-icon' }, icon), React.createElement('div', { className: 'attachment-info' }, React.createElement('div', { className: 'attachment-name' }, att.name), React.createElement('div', { className: 'attachment-size' }, formatSize(att.size || 0)) ), React.createElement('div', { className: 'attachment-actions' }, (isImage || isPdf) && React.createElement('button', { onClick: handlePreview, title: 'Prévisualiser' }, '\uD83D\uDC41'), React.createElement('button', { onClick: handleDownload, title: 'Télécharger' }, '\u2B07'), React.createElement('button', { onClick: () => onDelete(att.id), title: 'Supprimer' }, '\u2715') ) ); } function GrafcetEtape({ step, projectId, phaseColor, onUpdate, onDelete, onAddComment, onDeleteComment, onUpload, onDeleteAttachment, onPreview }) { const [expanded, setExpanded] = React.useState(false); const [editing, setEditing] = React.useState(false); const [editData, setEditData] = React.useState({}); const [commentText, setCommentText] = React.useState(''); const [submitting, setSubmitting] = React.useState(false); const [uploadProgress, setUploadProgress] = React.useState(null); // { name, pct, loaded, total } or null const fileRef = React.useRef(null); const status = STATUTS[step.status] || STATUTS.wait; const handleStatusChange = async (newStatus) => { try { await onUpdate(step.id, { status: newStatus }); Toasts.success(`Statut: ${STATUTS[newStatus].label}`); } catch (err) { Toasts.error(err.message); } }; const handleSaveEdit = async () => { try { await onUpdate(step.id, editData); setEditing(false); Toasts.success('Étape mise à jour'); } catch (err) { Toasts.error(err.message); } }; const handleAddComment = async () => { if (!commentText.trim()) return; setSubmitting(true); try { await onAddComment(step.id, commentText); setCommentText(''); Toasts.success('Commentaire ajouté'); } catch (err) { Toasts.error(err.message); } finally { setSubmitting(false); } }; const [dragging, setDragging] = React.useState(false); const processFiles = async (files) => { for (const file of files) { try { setUploadProgress({ name: file.name, pct: 0, loaded: 0, total: file.size }); await onUpload(step.id, file, (pct, loaded, total) => { setUploadProgress({ name: file.name, pct, loaded, total }); }); setUploadProgress(null); Toasts.success(`${file.name} uploadé`); } catch (err) { setUploadProgress(null); Toasts.error(`Erreur: ${file.name}`); } } }; const handleFileUpload = async (e) => { const files = e.target.files; if (!files.length) return; await processFiles(files); if (fileRef.current) fileRef.current.value = ''; }; const handleDrop = async (e) => { e.preventDefault(); e.stopPropagation(); setDragging(false); const files = e.dataTransfer.files; if (files.length) await processFiles(files); }; const handleDragOver = (e) => { e.preventDefault(); e.stopPropagation(); setDragging(true); }; const handleDragLeave = (e) => { e.preventDefault(); e.stopPropagation(); setDragging(false); }; const comments = step.comments || []; const attachments = step.attachments || []; return React.createElement('div', { className: 'grafcet-etape' }, React.createElement('div', { className: `etape-bloc status-${step.status}`, onClick: () => setExpanded(!expanded) }, React.createElement('div', { className: 'etape-drag', onClick: e => e.stopPropagation() }, '\u2630'), React.createElement('div', { className: 'etape-status-icon' }, status.icon), React.createElement('div', { className: 'etape-info' }, React.createElement('div', { className: 'etape-abbr' }, step.abbr || step.name), React.createElement('div', { className: 'etape-name', title: step.tooltip }, step.name) ), React.createElement('div', { className: 'etape-badges' }, comments.length > 0 && React.createElement('span', { className: 'etape-badge', style: { background: 'rgba(232,164,74,0.13)', color: 'var(--accent)' } }, '\uD83D\uDCAC ', comments.length), attachments.length > 0 && React.createElement('span', { className: 'etape-badge', style: { background: 'var(--purple-bg)', color: 'var(--purple)' } }, '\uD83D\uDCCE ', attachments.length) ), React.createElement('div', { className: `etape-expand-icon ${expanded ? 'open' : ''}` }, '\u25BC') ), expanded && React.createElement('div', { className: 'etape-details' }, // Info grid React.createElement('div', { className: 'etape-details-grid' }, React.createElement('div', { className: 'detail-field' }, React.createElement('label', null, 'Responsable'), editing ? React.createElement('input', { className: 'edit-input', value: editData.responsible || '', onChange: e => setEditData({...editData, responsible: e.target.value}) }) : React.createElement('div', { className: 'value' }, step.responsible || '—') ), React.createElement('div', { className: 'detail-field' }, React.createElement('label', null, 'Livrable'), editing ? React.createElement('input', { className: 'edit-input', value: editData.deliverable || '', onChange: e => setEditData({...editData, deliverable: e.target.value}) }) : React.createElement('div', { className: 'value' }, step.deliverable || '—') ), React.createElement('div', { className: 'detail-field' }, React.createElement('label', null, 'Statut'), React.createElement('select', { className: 'status-select', value: step.status, onChange: e => handleStatusChange(e.target.value), onClick: e => e.stopPropagation() }, Object.entries(STATUTS).map(([k, v]) => React.createElement('option', { key: k, value: k }, `${v.icon} ${v.label}`) ) ) ), React.createElement('div', { className: 'detail-field' }, React.createElement('label', null, 'Tooltip'), editing ? React.createElement('input', { className: 'edit-input', value: editData.tooltip || '', onChange: e => setEditData({...editData, tooltip: e.target.value}) }) : React.createElement('div', { className: 'value', style: { fontSize: '12px', color: 'var(--text-muted)' } }, step.tooltip || '—') ) ), // Edit actions React.createElement('div', { style: { display: 'flex', gap: '8px', marginBottom: '12px' } }, editing ? React.createElement(React.Fragment, null, React.createElement('button', { className: 'btn btn-success btn-sm', onClick: handleSaveEdit }, 'Sauvegarder'), React.createElement('button', { className: 'btn btn-ghost btn-sm', onClick: () => setEditing(false) }, 'Annuler') ) : React.createElement(React.Fragment, null, React.createElement('button', { className: 'btn btn-ghost btn-sm', onClick: () => { setEditing(true); setEditData({ name: step.name, abbr: step.abbr, tooltip: step.tooltip, responsible: step.responsible, deliverable: step.deliverable }); } }, '\u270E Modifier'), React.createElement('button', { className: 'btn btn-ghost btn-sm', style: { color: 'var(--red)' }, onClick: () => { if (confirm('Supprimer cette étape ?')) onDelete(step.id); } }, '\u2715 Supprimer') ) ), // Editing name/abbr editing && React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px', marginBottom: '12px' } }, React.createElement('div', { className: 'detail-field' }, React.createElement('label', null, 'Nom'), React.createElement('input', { className: 'edit-input', value: editData.name || '', onChange: e => setEditData({...editData, name: e.target.value}) }) ), React.createElement('div', { className: 'detail-field' }, React.createElement('label', null, 'Abréviation'), React.createElement('input', { className: 'edit-input', value: editData.abbr || '', onChange: e => setEditData({...editData, abbr: e.target.value}) }) ) ), // Attachments React.createElement('div', { className: 'attachments-section' }, React.createElement('div', { className: 'section-title' }, React.createElement('h4', null, '\uD83D\uDCCE Pièces jointes ', React.createElement('span', { className: 'count' }, attachments.length) ) ), attachments.map(att => React.createElement(AttachmentItem, { key: att.id, att, projectId, onDelete: (id) => onDeleteAttachment(id), onPreview: onPreview }) ), uploadProgress ? React.createElement('div', { className: 'upload-progress' }, React.createElement('div', { className: 'upload-progress-info' }, React.createElement('span', { className: 'upload-progress-name' }, uploadProgress.name), React.createElement('span', { className: 'upload-progress-pct' }, uploadProgress.pct + '%') ), React.createElement('div', { className: 'upload-progress-bar' }, React.createElement('div', { className: 'upload-progress-fill', style: { width: uploadProgress.pct + '%' } }) ), React.createElement('div', { className: 'upload-progress-size' }, formatSize(uploadProgress.loaded) + ' / ' + formatSize(uploadProgress.total) ) ) : React.createElement('label', { className: `upload-zone ${dragging ? 'drag-over' : ''}`, style: { display: 'block' }, onDrop: handleDrop, onDragOver: handleDragOver, onDragEnter: handleDragOver, onDragLeave: handleDragLeave }, React.createElement('p', null, dragging ? '\uD83D\uDCE5 Déposez vos fichiers ici' : '\u2795 Cliquez ou glissez des fichiers ici'), React.createElement('input', { ref: fileRef, type: 'file', multiple: true, style: { display: 'none' }, onChange: handleFileUpload }) ) ), // Comments React.createElement('div', { className: 'comments-section' }, React.createElement('div', { className: 'section-title' }, React.createElement('h4', null, '\uD83D\uDCAC Commentaires ', React.createElement('span', { className: 'count' }, comments.length) ) ), comments.map(c => React.createElement('div', { key: c.id, className: 'comment-item' }, React.createElement('div', { className: 'comment-header' }, React.createElement('span', { className: 'comment-author' }, c.author_name), React.createElement('span', null, React.createElement('span', { className: 'comment-date' }, c.date), React.createElement('button', { className: 'comment-delete', onClick: () => { if (confirm('Supprimer ce commentaire ?')) onDeleteComment(c.id); } }, ' \u2715') ) ), React.createElement('div', { className: 'comment-text' }, c.text) ) ), React.createElement('div', { className: 'comment-form' }, React.createElement('textarea', { value: commentText, placeholder: 'Ajouter un commentaire...', onChange: e => setCommentText(e.target.value), onKeyDown: e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleAddComment(); } } }), React.createElement('button', { className: 'btn btn-primary btn-sm', disabled: !commentText.trim() || submitting, onClick: handleAddComment }, '\u27A4') ) ) ) ); } function PhaseGrafcet({ phase, projectId, onUpdateStep, onDeleteStep, onAddStep, onAddComment, onDeleteComment, onUpload, onDeleteAttachment, onUpdatePhase, onDeletePhase, onPreview }) { const [collapsed, setCollapsed] = React.useState(false); const [editingPhase, setEditingPhase] = React.useState(false); const [phaseName, setPhaseName] = React.useState(phase.name); const [phaseFullName, setPhaseFullName] = React.useState(phase.full_name); const steps = phase.steps || []; const doneCount = steps.filter(s => s.status === 'done').length; const activeCount = steps.filter(s => s.status === 'active').length; const handleSavePhase = async () => { try { await onUpdatePhase(phase.id, { name: phaseName, full_name: phaseFullName }); setEditingPhase(false); Toasts.success('Phase mise à jour'); } catch (err) { Toasts.error(err.message); } }; return React.createElement('div', { className: 'phase-container', id: `phase-${phase.id}` }, React.createElement('div', { className: 'phase-header', style: { background: `linear-gradient(135deg, ${phase.color}, ${phase.color}88)` }, onClick: () => setCollapsed(!collapsed) }, React.createElement('div', { className: 'phase-header-left' }, React.createElement('span', { className: `phase-chevron ${!collapsed ? 'open' : ''}` }, '\u25B6'), editingPhase ? React.createElement('div', { className: 'inline-edit-row', onClick: e => e.stopPropagation() }, React.createElement('input', { className: 'edit-input', value: phaseName, onChange: e => setPhaseName(e.target.value), style: { width: '150px' } }), React.createElement('input', { className: 'edit-input', value: phaseFullName, onChange: e => setPhaseFullName(e.target.value), style: { width: '200px' }, placeholder: 'Nom complet' }), React.createElement('button', { className: 'btn btn-sm', style: { background: 'rgba(255,255,255,0.2)', color: 'white', border: 'none' }, onClick: handleSavePhase }, '\u2713'), React.createElement('button', { className: 'btn btn-sm', style: { background: 'rgba(255,255,255,0.1)', color: 'white', border: 'none' }, onClick: () => setEditingPhase(false) }, '\u2715') ) : React.createElement(React.Fragment, null, React.createElement('span', { className: 'phase-name' }, phase.ext_id, ' \u2014 ', phase.name), phase.full_name && React.createElement('span', { className: 'phase-fullname' }, phase.full_name) ) ), React.createElement('div', { className: 'phase-header-right' }, React.createElement('span', { className: 'phase-stats' }, doneCount, '/', steps.length, activeCount > 0 ? ` \u2022 ${activeCount} en cours` : '' ), React.createElement('div', { className: 'phase-actions', onClick: e => e.stopPropagation() }, React.createElement('button', { onClick: () => onAddStep(phase.id) }, '+'), React.createElement('button', { onClick: () => { setEditingPhase(true); setPhaseName(phase.name); setPhaseFullName(phase.full_name); } }, '\u270E'), React.createElement('button', { onClick: () => { if (confirm(`Supprimer la phase "${phase.name}" ?`)) onDeletePhase(phase.id); } }, '\u2715') ) ) ), !collapsed && React.createElement('div', { className: 'phase-body' }, steps.length === 0 ? React.createElement('div', { style: { textAlign: 'center', padding: '20px', color: 'var(--text-muted)' } }, 'Aucune étape') : React.createElement('div', { className: 'phase-steps' }, steps.map((step, i) => React.createElement(React.Fragment, { key: step.id }, React.createElement(GrafcetEtape, { step, projectId, phaseColor: phase.color, onUpdate: onUpdateStep, onDelete: onDeleteStep, onAddComment, onDeleteComment, onUpload, onDeleteAttachment, onPreview }) ) ) ) ) ); } function MiniGrafcet({ phases, onSelectPhase }) { return React.createElement('div', { className: 'mini-grafcet' }, React.createElement('h4', { style: { fontSize: '11px', fontWeight: '700', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '1px', marginBottom: '12px' } }, 'Vue synoptique'), phases.map((phase, i) => { const steps = phase.steps || []; const done = steps.filter(s => s.status === 'done').length; const hasActive = steps.some(s => s.status === 'active'); const pct = steps.length > 0 ? (done / steps.length) * 100 : 0; return React.createElement(React.Fragment, { key: phase.id }, React.createElement('div', { className: `mini-phase ${hasActive ? 'active' : ''}`, onClick: () => onSelectPhase(phase.id) }, React.createElement('div', { className: 'mini-phase-header' }, React.createElement('span', { className: 'mini-phase-name', style: { color: phase.color } }, phase.ext_id ), React.createElement('span', { className: 'mini-phase-count' }, `${done}/${steps.length}`) ), React.createElement('div', { className: 'mini-phase-bar' }, React.createElement('div', { className: 'mini-phase-bar-fill', style: { width: `${pct}%`, backgroundColor: phase.color } }) ) ) ); }) ); }