diff --git a/backend/app/main.py b/backend/app/main.py index 11d38bd..d59d7c7 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -203,7 +203,7 @@ def send_completed_inspection_to_n8n(inspection, db): # No lanzamos excepción para no interrumpir el flujo normal -BACKEND_VERSION = "1.0.27" +BACKEND_VERSION = "1.0.28" app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION) # S3/MinIO configuration @@ -878,6 +878,71 @@ def update_checklist( return db_checklist +@app.post("/api/checklists/{checklist_id}/upload-logo") +async def upload_checklist_logo( + checklist_id: int, + file: UploadFile = File(...), + db: Session = Depends(get_db), + current_user: models.User = Depends(get_current_user) +): + """Subir logo para un checklist (solo admin)""" + if current_user.role != "admin": + raise HTTPException(status_code=403, detail="Solo administradores pueden subir logos") + + # Verificar que el checklist existe + checklist = db.query(models.Checklist).filter(models.Checklist.id == checklist_id).first() + if not checklist: + raise HTTPException(status_code=404, detail="Checklist no encontrado") + + # Validar que es una imagen + if not file.content_type.startswith('image/'): + raise HTTPException(status_code=400, detail="El archivo debe ser una imagen") + + # Subir a S3/MinIO + file_extension = file.filename.split(".")[-1] + now = datetime.now() + folder = f"checklist-logos/{now.year}/{now.month:02d}" + file_name = f"checklist_{checklist_id}_{uuid.uuid4().hex}.{file_extension}" + s3_key = f"{folder}/{file_name}" + + file_content = await file.read() + s3_client.upload_fileobj( + BytesIO(file_content), + S3_IMAGE_BUCKET, + s3_key, + ExtraArgs={"ContentType": file.content_type} + ) + + logo_url = f"{S3_ENDPOINT}/{S3_IMAGE_BUCKET}/{s3_key}" + + # Actualizar checklist + checklist.logo_url = logo_url + db.commit() + db.refresh(checklist) + + return {"logo_url": logo_url, "message": "Logo subido exitosamente"} + + +@app.delete("/api/checklists/{checklist_id}/logo") +def delete_checklist_logo( + checklist_id: int, + db: Session = Depends(get_db), + current_user: models.User = Depends(get_current_user) +): + """Eliminar logo de un checklist (solo admin)""" + if current_user.role != "admin": + raise HTTPException(status_code=403, detail="Solo administradores pueden eliminar logos") + + checklist = db.query(models.Checklist).filter(models.Checklist.id == checklist_id).first() + if not checklist: + raise HTTPException(status_code=404, detail="Checklist no encontrado") + + checklist.logo_url = None + db.commit() + + return {"message": "Logo eliminado exitosamente"} + + # ============= QUESTION ENDPOINTS ============= @app.post("/api/questions", response_model=schemas.Question) def create_question( diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 890791a..b7d9e83 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1458,9 +1458,11 @@ function ChecklistsTab({ checklists, user, onChecklistCreated, onStartInspection const [showCreateModal, setShowCreateModal] = useState(false) const [showQuestionsModal, setShowQuestionsModal] = useState(false) const [showEditPermissionsModal, setShowEditPermissionsModal] = useState(false) + const [showLogoModal, setShowLogoModal] = useState(false) const [selectedChecklist, setSelectedChecklist] = useState(null) const [creating, setCreating] = useState(false) const [updating, setUpdating] = useState(false) + const [uploadingLogo, setUploadingLogo] = useState(false) const [mechanics, setMechanics] = useState([]) const [searchTerm, setSearchTerm] = useState('') const [aiModeFilter, setAiModeFilter] = useState('all') // all, off, optional, required @@ -1583,6 +1585,78 @@ function ChecklistsTab({ checklists, user, onChecklistCreated, onStartInspection } } + const handleUploadLogo = async (e) => { + const file = e.target.files[0] + if (!file) return + + // Validar que sea imagen + if (!file.type.startsWith('image/')) { + alert('Por favor selecciona una imagen válida') + return + } + + setUploadingLogo(true) + + try { + const token = localStorage.getItem('token') + const API_URL = import.meta.env.VITE_API_URL || '' + + const formData = new FormData() + formData.append('file', file) + + const response = await fetch(`${API_URL}/api/checklists/${selectedChecklist.id}/upload-logo`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}` + }, + body: formData + }) + + if (response.ok) { + setShowLogoModal(false) + setSelectedChecklist(null) + onChecklistCreated() // Reload checklists + alert('Logo subido exitosamente') + } else { + const error = await response.json() + alert(error.detail || 'Error al subir el logo') + } + } catch (error) { + console.error('Error:', error) + alert('Error al subir el logo') + } finally { + setUploadingLogo(false) + } + } + + const handleDeleteLogo = async () => { + if (!confirm('¿Estás seguro de eliminar el logo?')) return + + try { + const token = localStorage.getItem('token') + const API_URL = import.meta.env.VITE_API_URL || '' + + const response = await fetch(`${API_URL}/api/checklists/${selectedChecklist.id}/logo`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${token}` + } + }) + + if (response.ok) { + setShowLogoModal(false) + setSelectedChecklist(null) + onChecklistCreated() // Reload checklists + alert('Logo eliminado exitosamente') + } else { + alert('Error al eliminar el logo') + } + } catch (error) { + console.error('Error:', error) + alert('Error al eliminar el logo') + } + } + return (
{user.role === 'admin' && ( @@ -1669,7 +1743,22 @@ function ChecklistsTab({ checklists, user, onChecklistCreated, onStartInspection ) : ( filteredChecklists.map((checklist) => (
-
+
+ {/* Logo del Checklist */} +
+ {checklist.logo_url ? ( + {`Logo + ) : ( +
+ 📋 +
+ )} +
+

{checklist.name}

{checklist.description}

@@ -1701,6 +1790,16 @@ function ChecklistsTab({ checklists, user, onChecklistCreated, onStartInspection
{user.role === 'admin' && ( <> +
)} + + {/* Modal Gestionar Logo */} + {showLogoModal && selectedChecklist && ( +
+
+
+

Gestionar Logo

+

+ {selectedChecklist.name} +

+ + {/* Logo actual */} +
+ {selectedChecklist.logo_url ? ( +
+ Logo actual +
+ ) : ( +
+ 📋 +
+ )} +
+ + {/* Botones de acción */} +
+ + + {selectedChecklist.logo_url && ( + + )} + + +
+ +

+ 💡 Tamaño recomendado: 200x200px o similar. Formatos: JPG, PNG, SVG +

+
+
+
+ )}
) }