diff --git a/__pycache__/admin_routes.cpython-312.pyc b/__pycache__/admin_routes.cpython-312.pyc index 4bd126b..703d679 100644 Binary files a/__pycache__/admin_routes.cpython-312.pyc and b/__pycache__/admin_routes.cpython-312.pyc differ diff --git a/__pycache__/config.cpython-312.pyc b/__pycache__/config.cpython-312.pyc index 372c1ed..ec3cb40 100644 Binary files a/__pycache__/config.cpython-312.pyc and b/__pycache__/config.cpython-312.pyc differ diff --git a/__pycache__/database.cpython-312.pyc b/__pycache__/database.cpython-312.pyc index 61b3b4d..bf736e9 100644 Binary files a/__pycache__/database.cpython-312.pyc and b/__pycache__/database.cpython-312.pyc differ diff --git a/__pycache__/image_processor.cpython-312.pyc b/__pycache__/image_processor.cpython-312.pyc index c8e7343..711c294 100644 Binary files a/__pycache__/image_processor.cpython-312.pyc and b/__pycache__/image_processor.cpython-312.pyc differ diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc index 61615af..be9a38d 100644 Binary files a/__pycache__/main.cpython-312.pyc and b/__pycache__/main.cpython-312.pyc differ diff --git a/__pycache__/models.cpython-312.pyc b/__pycache__/models.cpython-312.pyc index abf0787..3ed9b86 100644 Binary files a/__pycache__/models.cpython-312.pyc and b/__pycache__/models.cpython-312.pyc differ diff --git a/__pycache__/mqtt_manager.cpython-312.pyc b/__pycache__/mqtt_manager.cpython-312.pyc index 5224f92..0da7ebb 100644 Binary files a/__pycache__/mqtt_manager.cpython-312.pyc and b/__pycache__/mqtt_manager.cpython-312.pyc differ diff --git a/__pycache__/schemas.cpython-312.pyc b/__pycache__/schemas.cpython-312.pyc index 7dd38c6..f4433b5 100644 Binary files a/__pycache__/schemas.cpython-312.pyc and b/__pycache__/schemas.cpython-312.pyc differ diff --git a/admin_routes.py b/admin_routes.py index 2f2a4a8..8f324cd 100644 --- a/admin_routes.py +++ b/admin_routes.py @@ -6,10 +6,11 @@ from typing import Optional, List import json import os import secrets +from datetime import datetime from database import get_db -from models import Device as DeviceModel, Content as ContentModel -from schemas import DeviceCreate, ContentCreate +from models import Device as DeviceModel, Content as ContentModel, Todo as TodoModel +from schemas import DeviceCreate, ContentCreate, TodoCreate, TodoUpdate from image_processor import image_processor from mqtt_manager import mqtt_manager @@ -31,22 +32,33 @@ async def admin_dashboard(request: Request, db: Session = Depends(get_db)): # 获取内容数量 content_count = db.query(ContentModel).count() - active_content_count = db.query(ContentModel).filter(ContentModel.is_active == True).count() + + # 获取待办事项数量 + todo_count = db.query(TodoModel).count() + completed_todo_count = db.query(TodoModel).filter(TodoModel.is_completed == True).count() + pending_todo_count = todo_count - completed_todo_count # 获取最近上线的设备 recent_devices = db.query(DeviceModel).order_by(DeviceModel.last_online.desc()).limit(5).all() - # 获取最近创建的内容 - recent_contents = db.query(ContentModel).order_by(ContentModel.created_at.desc()).limit(5).all() + # 获取最近创建的待办事项 + recent_todos_query = db.query(TodoModel, DeviceModel).join( + DeviceModel, TodoModel.device_id == DeviceModel.device_id + ).order_by(TodoModel.created_at.desc()).limit(5).all() + + # 转换为包含todo和device的对象列表 + recent_todos = [{"todo": todo, "device": device} for todo, device in recent_todos_query] return templates.TemplateResponse("admin/dashboard.html", { "request": request, "device_count": device_count, "active_device_count": active_device_count, "content_count": content_count, - "active_content_count": active_content_count, + "todo_count": todo_count, + "completed_todo_count": completed_todo_count, + "pending_todo_count": pending_todo_count, "recent_devices": recent_devices, - "recent_contents": recent_contents + "recent_todos": recent_todos }) @admin_router.get("/devices", response_class=HTMLResponse) @@ -341,4 +353,262 @@ async def upload_image(request: Request, db: Session = Depends(get_db)): "request": request, "devices": devices, "error": f"图片处理失败: {str(e)}" - }) \ No newline at end of file + }) + +# 待办事项管理路由 +@admin_router.get("/todos", response_class=HTMLResponse) +async def todos_list(request: Request, device_id: Optional[str] = None, db: Session = Depends(get_db)): + """ + 待办事项列表页面 + """ + if device_id: + # 获取特定设备的待办事项 + device = db.query(DeviceModel).filter(DeviceModel.device_id == device_id).first() + if not device: + raise HTTPException(status_code=404, detail="设备不存在") + + todos = db.query(TodoModel).filter(TodoModel.device_id == device_id).order_by(TodoModel.created_at.desc()).all() + return templates.TemplateResponse("admin/todos.html", { + "request": request, + "todos": todos, + "device": device, + "filtered": True + }) + else: + # 获取所有待办事项 + todos = db.query(TodoModel).order_by(TodoModel.created_at.desc()).all() + devices = db.query(DeviceModel).all() + + # 为每个待办事项添加设备信息 + todo_list = [] + for todo in todos: + device = db.query(DeviceModel).filter(DeviceModel.device_id == todo.device_id).first() + todo_list.append({ + "todo": todo, + "device": device + }) + + return templates.TemplateResponse("admin/todos.html", { + "request": request, + "todo_list": todo_list, + "devices": devices, + "filtered": False + }) + +@admin_router.get("/todos/add", response_class=HTMLResponse) +@admin_router.post("/todos/add", response_class=HTMLResponse) +async def add_todo(request: Request, device_id: Optional[str] = None, db: Session = Depends(get_db)): + """ + 添加待办事项页面和处理 + """ + if request.method == "GET": + devices = db.query(DeviceModel).filter(DeviceModel.is_active == True).all() + return templates.TemplateResponse("admin/todo_add.html", { + "request": request, + "devices": devices, + "selected_device": device_id + }) + + # 处理POST请求 + form = await request.form() + device_id = form.get("device_id") + title = form.get("title") + description = form.get("description") + due_date_str = form.get("due_date") + + # 检查设备是否存在 + device = db.query(DeviceModel).filter(DeviceModel.device_id == device_id).first() + if not device: + devices = db.query(DeviceModel).filter(DeviceModel.is_active == True).all() + return templates.TemplateResponse("admin/todo_add.html", { + "request": request, + "devices": devices, + "error": "设备不存在" + }) + + # 处理截止日期 + due_date = None + if due_date_str: + try: + from datetime import datetime + due_date = datetime.strptime(due_date_str, "%Y-%m-%dT%H:%M") + except ValueError: + devices = db.query(DeviceModel).filter(DeviceModel.is_active == True).all() + return templates.TemplateResponse("admin/todo_add.html", { + "request": request, + "devices": devices, + "error": "截止日期格式不正确" + }) + + # 创建新的待办事项 + new_todo = TodoModel( + title=title, + description=description, + device_id=device_id, + due_date=due_date + ) + + db.add(new_todo) + db.commit() + + # 发送MQTT通知给设备 + mqtt_manager.send_todo_command(device_id, "create", { + "id": new_todo.id, + "title": title, + "description": description, + "due_date": due_date.isoformat() if due_date else None + }) + + return RedirectResponse(url="/admin/todos", status_code=303) + +@admin_router.get("/todos/{todo_id}", response_class=HTMLResponse) +async def todo_detail(request: Request, todo_id: int, db: Session = Depends(get_db)): + """ + 待办事项详情页面 + """ + todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first() + if not todo: + raise HTTPException(status_code=404, detail="待办事项不存在") + + # 获取设备信息 + device = db.query(DeviceModel).filter(DeviceModel.device_id == todo.device_id).first() + + return templates.TemplateResponse("admin/todo_detail.html", { + "request": request, + "todo": todo, + "device": device + }) + +@admin_router.post("/todos/{todo_id}/toggle", response_class=HTMLResponse) +async def toggle_todo_status(todo_id: int, db: Session = Depends(get_db)): + """ + 切换待办事项完成状态 + """ + todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first() + if not todo: + raise HTTPException(status_code=404, detail="待办事项不存在") + + # 切换状态 + todo.is_completed = not todo.is_completed + if todo.is_completed: + todo.completed_at = datetime.utcnow() + else: + todo.completed_at = None + + todo.updated_at = datetime.utcnow() + db.commit() + + # 发送MQTT通知给设备 + mqtt_manager.send_todo_command(todo.device_id, "update", { + "id": todo.id, + "is_completed": todo.is_completed, + "completed_at": todo.completed_at.isoformat() if todo.completed_at else None + }) + + return RedirectResponse(url=f"/admin/todos/{todo_id}", status_code=303) + +@admin_router.get("/todos/{todo_id}/edit", response_class=HTMLResponse) +async def edit_todo_page(request: Request, todo_id: int, db: Session = Depends(get_db)): + todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first() + if not todo: + raise HTTPException(status_code=404, detail="待办事项不存在") + + devices = db.query(DeviceModel).all() + + return templates.TemplateResponse("admin/todo_edit.html", { + "request": request, + "todo": todo, + "devices": devices + }) + +@admin_router.post("/todos/{todo_id}/edit") +async def edit_todo( + request: Request, + todo_id: int, + title: str = Form(...), + description: str = Form(""), + device_id: str = Form(...), + due_date: Optional[str] = Form(None), + is_completed: bool = Form(False), + db: Session = Depends(get_db) +): + todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first() + if not todo: + raise HTTPException(status_code=404, detail="待办事项不存在") + + device = db.query(DeviceModel).filter(DeviceModel.device_id == device_id).first() + if not device: + raise HTTPException(status_code=404, detail="设备不存在") + + # 更新待办事项 + todo.title = title + todo.description = description if description else None + todo.device_id = device_id + todo.due_date = datetime.fromisoformat(due_date) if due_date else None + + # 检查完成状态变化 + was_completed = todo.is_completed + todo.is_completed = is_completed + + # 如果从未完成变为已完成,设置完成时间 + if not was_completed and is_completed: + todo.completed_at = datetime.utcnow() + # 如果从已完成变为未完成,清除完成时间 + elif was_completed and not is_completed: + todo.completed_at = None + + todo.updated_at = datetime.utcnow() + + db.commit() + + # 发送MQTT通知到设备 + if hasattr(request.app.state, 'mqtt_manager') and request.app.state.mqtt_manager: + try: + await request.app.state.mqtt_manager.send_todo_command( + device_id=device_id, + action="update", + todo_data={ + "id": todo.id, + "title": todo.title, + "description": todo.description, + "due_date": todo.due_date.isoformat() if todo.due_date else None, + "is_completed": todo.is_completed + } + ) + except Exception as e: + print(f"发送待办事项更新MQTT消息失败: {e}") + + return RedirectResponse(url=f"/admin/todos/{todo_id}", status_code=303) + +@admin_router.post("/todos/{todo_id}/delete") +async def delete_todo( + request: Request, + todo_id: int, + db: Session = Depends(get_db) +): + """ + 删除待办事项 + """ + todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first() + if not todo: + raise HTTPException(status_code=404, detail="待办事项不存在") + + device_id = todo.device_id + + # 发送MQTT通知到设备 + if hasattr(request.app.state, 'mqtt_manager') and request.app.state.mqtt_manager: + try: + await request.app.state.mqtt_manager.send_todo_command( + device_id=device_id, + action="delete", + todo_data={ + "id": todo.id + } + ) + except Exception as e: + print(f"发送待办事项删除MQTT消息失败: {e}") + + db.delete(todo) + db.commit() + + return RedirectResponse(url=f"/admin/todos?device_id={device_id}", status_code=303) \ No newline at end of file diff --git a/api/__init__.py b/api/__init__.py index dd27b23..abb40a1 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -1,8 +1,9 @@ from fastapi import APIRouter -from api import devices, contents +from api import devices, contents, todos api_router = APIRouter() # 注册所有路由 api_router.include_router(devices.router) -api_router.include_router(contents.router) \ No newline at end of file +api_router.include_router(contents.router) +api_router.include_router(todos.router) \ No newline at end of file diff --git a/api/__pycache__/__init__.cpython-312.pyc b/api/__pycache__/__init__.cpython-312.pyc index bc30392..6178234 100644 Binary files a/api/__pycache__/__init__.cpython-312.pyc and b/api/__pycache__/__init__.cpython-312.pyc differ diff --git a/api/__pycache__/contents.cpython-312.pyc b/api/__pycache__/contents.cpython-312.pyc index 6d35d26..cc16a33 100644 Binary files a/api/__pycache__/contents.cpython-312.pyc and b/api/__pycache__/contents.cpython-312.pyc differ diff --git a/api/__pycache__/devices.cpython-312.pyc b/api/__pycache__/devices.cpython-312.pyc index 6d00788..32f8433 100644 Binary files a/api/__pycache__/devices.cpython-312.pyc and b/api/__pycache__/devices.cpython-312.pyc differ diff --git a/api/__pycache__/todos.cpython-312.pyc b/api/__pycache__/todos.cpython-312.pyc new file mode 100644 index 0000000..dc425f4 Binary files /dev/null and b/api/__pycache__/todos.cpython-312.pyc differ diff --git a/api/todos.py b/api/todos.py new file mode 100644 index 0000000..fbeb654 --- /dev/null +++ b/api/todos.py @@ -0,0 +1,168 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import List, Optional +from datetime import datetime + +from database import get_db +from schemas import Todo as TodoSchema, TodoCreate, TodoUpdate +from models import Todo as TodoModel +from database import Device as DeviceModel + +router = APIRouter( + prefix="/api/todos", + tags=["todos"] +) + +@router.post("/", response_model=TodoSchema, status_code=status.HTTP_201_CREATED) +async def create_todo(todo: TodoCreate, db: Session = Depends(get_db)): + """ + 创建新的待办事项 + """ + # 检查设备是否存在 + device = db.query(DeviceModel).filter(DeviceModel.device_id == todo.device_id).first() + if not device: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="设备不存在" + ) + + # 创建新的待办事项 + db_todo = TodoModel( + title=todo.title, + description=todo.description, + device_id=todo.device_id, + due_date=todo.due_date + ) + + db.add(db_todo) + db.commit() + db.refresh(db_todo) + + return db_todo + +@router.get("/", response_model=List[TodoSchema]) +async def list_todos( + skip: int = 0, + limit: int = 100, + device_id: Optional[str] = None, + is_completed: Optional[bool] = None, + db: Session = Depends(get_db) +): + """ + 获取待办事项列表 + """ + query = db.query(TodoModel) + + if device_id: + query = query.filter(TodoModel.device_id == device_id) + + if is_completed is not None: + query = query.filter(TodoModel.is_completed == is_completed) + + todos = query.order_by(TodoModel.created_at.desc()).offset(skip).limit(limit).all() + return todos + +@router.get("/{todo_id}", response_model=TodoSchema) +async def get_todo(todo_id: int, db: Session = Depends(get_db)): + """ + 获取待办事项详情 + """ + todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first() + if not todo: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="待办事项不存在" + ) + return todo + +@router.put("/{todo_id}", response_model=TodoSchema) +async def update_todo( + todo_id: int, + todo_update: TodoUpdate, + db: Session = Depends(get_db) +): + """ + 更新待办事项 + """ + todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first() + if not todo: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="待办事项不存在" + ) + + # 更新待办事项信息 + update_data = todo_update.model_dump(exclude_unset=True) + + # 如果状态从未完成变为完成,设置完成时间 + if "is_completed" in update_data and update_data["is_completed"] and not todo.is_completed: + update_data["completed_at"] = datetime.utcnow() + # 如果状态从完成变为未完成,清除完成时间 + elif "is_completed" in update_data and not update_data["is_completed"] and todo.is_completed: + update_data["completed_at"] = None + + for field, value in update_data.items(): + setattr(todo, field, value) + + todo.updated_at = datetime.utcnow() + db.commit() + db.refresh(todo) + + return todo + +@router.delete("/{todo_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_todo(todo_id: int, db: Session = Depends(get_db)): + """ + 删除待办事项 + """ + todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first() + if not todo: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="待办事项不存在" + ) + + db.delete(todo) + db.commit() + +@router.post("/{todo_id}/complete", response_model=TodoSchema) +async def complete_todo(todo_id: int, db: Session = Depends(get_db)): + """ + 标记待办事项为完成 + """ + todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first() + if not todo: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="待办事项不存在" + ) + + todo.is_completed = True + todo.completed_at = datetime.utcnow() + todo.updated_at = datetime.utcnow() + + db.commit() + db.refresh(todo) + + return todo + +@router.post("/{todo_id}/incomplete", response_model=TodoSchema) +async def incomplete_todo(todo_id: int, db: Session = Depends(get_db)): + """ + 标记待办事项为未完成 + """ + todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first() + if not todo: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="待办事项不存在" + ) + + todo.is_completed = False + todo.completed_at = None + todo.updated_at = datetime.utcnow() + + db.commit() + db.refresh(todo) + + return todo \ No newline at end of file diff --git a/database.py b/database.py index cd5fd0d..c1b4ea1 100644 --- a/database.py +++ b/database.py @@ -40,6 +40,22 @@ class Content(Base): # 关联设备 device = relationship("Device", back_populates="contents") +class Todo(Base): + __tablename__ = "todos" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String(200), nullable=False) + description = Column(Text, nullable=True) + device_id = Column(String(50), ForeignKey("devices.device_id"), nullable=False) + is_completed = Column(Boolean, default=False) + due_date = Column(DateTime, nullable=True) + completed_at = Column(DateTime, nullable=True) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # 关联设备 + device = relationship("Device") + # 创建数据库连接 engine = create_engine(settings.database_url) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) diff --git a/models.py b/models.py index c180430..bc29898 100644 --- a/models.py +++ b/models.py @@ -2,6 +2,6 @@ from sqlalchemy.orm import Session from database import Base # 导入所有模型以确保它们被注册到Base.metadata -from database import Device, Content +from database import Device, Content, Todo -__all__ = ["Device", "Content"] \ No newline at end of file +__all__ = ["Device", "Content", "Todo"] \ No newline at end of file diff --git a/mqtt_manager.py b/mqtt_manager.py index d1306fb..d8eb282 100644 --- a/mqtt_manager.py +++ b/mqtt_manager.py @@ -165,6 +165,43 @@ class MQTTManager: logger.info(f"已取消订阅设备 {device_id} 状态") except Exception as e: logger.error(f"取消订阅设备状态失败: {str(e)}") + + def send_todo_command(self, device_id: str, action: str, todo_data: Dict[str, Any]) -> bool: + """ + 发送待办事项命令 + + Args: + device_id: 设备ID + action: 动作类型 (create, update, delete) + todo_data: 待办事项数据 + + Returns: + 是否发送成功 + """ + if not self.connected: + logger.error("MQTT未连接,无法发送待办事项命令") + return False + + try: + topic = f"esp32/{device_id}/todo" + payload = { + "type": "todo", + "action": action, + "data": todo_data, + "timestamp": int(time.time()) + } + + result = self.client.publish(topic, json.dumps(payload)) + if result.rc == mqtt.MQTT_ERR_SUCCESS: + logger.info(f"成功向设备 {device_id} 发送待办事项命令: {action}") + return True + else: + logger.error(f"向设备 {device_id} 发送待办事项命令失败,错误码: {result.rc}") + return False + + except Exception as e: + logger.error(f"发送待办事项命令失败: {str(e)}") + return False # 全局MQTT管理器实例 mqtt_manager = MQTTManager() \ No newline at end of file diff --git a/schemas.py b/schemas.py index 9bd92a5..982caed 100644 --- a/schemas.py +++ b/schemas.py @@ -86,4 +86,30 @@ class MQTTStatus(BaseModel): content_version: Optional[int] = None timestamp: int = Field(..., description="时间戳") device_id: str = Field(..., description="设备ID") - message: Optional[str] = None \ No newline at end of file + message: Optional[str] = None + +# 待办事项相关模型 +class TodoBase(BaseModel): + title: str = Field(..., description="待办事项标题") + description: Optional[str] = Field(None, description="待办事项描述") + due_date: Optional[datetime] = Field(None, description="截止日期") + +class TodoCreate(TodoBase): + device_id: str = Field(..., description="设备ID") + +class TodoUpdate(BaseModel): + title: Optional[str] = None + description: Optional[str] = None + due_date: Optional[datetime] = None + is_completed: Optional[bool] = None + +class Todo(TodoBase): + id: int + device_id: str + is_completed: bool + completed_at: Optional[datetime] = None + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True \ No newline at end of file diff --git a/templates/admin/base.html b/templates/admin/base.html index 1b6a294..06a0394 100644 --- a/templates/admin/base.html +++ b/templates/admin/base.html @@ -35,6 +35,11 @@ 内容管理 +
活跃内容
-待办事项
+待完成
+| 设备ID | 标题 | -版本 | +设备 | +状态 | 创建时间 | ||
|---|---|---|---|---|---|---|---|
{{ content.device_id }} |
- {{ content.title }} | -v{{ content.version }} | -{{ content.created_at.strftime('%Y-%m-%d %H:%M') }} | +{{ todo.title }} | +{{ device.name or device.device_id }} | ++ {% if todo.is_completed %} + 已完成 + {% else %} + 待完成 + {% endif %} + | +{{ todo.created_at.strftime('%Y-%m-%d %H:%M') }} |
| 标题 | +描述 | +截止时间 | +状态 | +创建时间 | +操作 | +
|---|---|---|---|---|---|
| + + {{ todo.title }} + + | +{{ todo.description or '-' }} | ++ {% if todo.due_date %} + {{ todo.due_date.strftime('%Y-%m-%d %H:%M') }} + {% else %} + - + {% endif %} + | ++ {% if todo.is_completed %} + 已完成 + {% if todo.completed_at %} + {{ todo.completed_at.strftime('%Y-%m-%d %H:%M') }} + {% endif %} + {% else %} + 未完成 + {% endif %} + | +{{ todo.created_at.strftime('%Y-%m-%d %H:%M') }} | +
+
+
+
+
+
+
+
+ |
+
| 标题 | +设备 | +描述 | +截止时间 | +状态 | +创建时间 | +操作 | +
|---|---|---|---|---|---|---|
| + + {{ item.todo.title }} + + | ++ + {{ item.device.name or item.device.device_id }} + + | +{{ item.todo.description or '-' }} | ++ {% if item.todo.due_date %} + {{ item.todo.due_date.strftime('%Y-%m-%d %H:%M') }} + {% else %} + - + {% endif %} + | ++ {% if item.todo.is_completed %} + 已完成 + {% if item.todo.completed_at %} + {{ item.todo.completed_at.strftime('%Y-%m-%d %H:%M') }} + {% endif %} + {% else %} + 未完成 + {% endif %} + | +{{ item.todo.created_at.strftime('%Y-%m-%d %H:%M') }} | +
+
+
+
+
+
+
+
+ |
+