from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File from sqlmodel import Session, select, func from typing import List, Optional from app.database import get_session from app.models import ( KanbanBoard, KanbanColumn, KanbanCard, Channel, User, KanbanChecklist, KanbanChecklistItem, UserRole, KanbanCardComment, KanbanCardAttachment, KanbanTimeEntry, KanbanCustomField, KanbanCustomFieldValue, KanbanCardTemplate, KanbanCardActivityLog ) from app.schemas import ( KanbanBoardCreate, KanbanBoardUpdate, KanbanBoardResponse, KanbanColumnCreate, KanbanColumnUpdate, KanbanColumnResponse, KanbanCardCreate, KanbanCardUpdate, KanbanCardResponse, KanbanBoardWithColumns, KanbanColumnWithCards, KanbanChecklistCreate, KanbanChecklistUpdate, KanbanChecklistResponse, KanbanChecklistItemCreate, KanbanChecklistItemUpdate, KanbanChecklistItemResponse, KanbanChecklistWithItems, KanbanCardWithChecklists, KanbanCardCommentCreate, KanbanCardCommentUpdate, KanbanCardCommentResponse, KanbanCardAttachmentResponse, KanbanTimeEntryCreate, KanbanTimeEntryUpdate, KanbanTimeEntryResponse, KanbanCustomFieldCreate, KanbanCustomFieldUpdate, KanbanCustomFieldResponse, KanbanCustomFieldValueCreate, KanbanCustomFieldValueUpdate, KanbanCustomFieldValueResponse, KanbanCardTemplateCreate, KanbanCardTemplateUpdate, KanbanCardTemplateResponse, KanbanCardExtendedResponse, KanbanBoardExtendedResponse ) from app.auth import get_current_user import os from pathlib import Path from datetime import datetime router = APIRouter(prefix="/kanban", tags=["Kanban"]) # Helper function to log card activity def log_card_activity( session: Session, card_id: int, user_id: int, action: str, field_name: Optional[str] = None, old_value: Optional[str] = None, new_value: Optional[str] = None ): """Log a card activity for the activity log""" try: # Convert assignee_id to names for better readability if field_name == 'assignee_id': old_name = None new_name = None if old_value and old_value != 'None': try: old_user = session.get(User, int(old_value)) old_name = old_user.full_name if old_user else f"User {old_value}" except: old_name = str(old_value) if new_value and new_value != 'None': try: new_user = session.get(User, int(new_value)) new_name = new_user.full_name if new_user else f"User {new_value}" except: new_name = str(new_value) old_value = old_name or "Nicht zugewiesen" new_value = new_name or "Nicht zugewiesen" field_name = 'Zugewiesen an' activity = KanbanCardActivityLog( card_id=card_id, user_id=user_id, action=action, field_name=field_name, old_value=str(old_value) if old_value is not None else None, new_value=str(new_value) if new_value is not None else None ) session.add(activity) session.commit() except Exception as e: # Log errors but don't fail the main operation print(f"Failed to log activity: {e}") # Board endpoints @router.post("/boards", response_model=KanbanBoardResponse, status_code=status.HTTP_201_CREATED) def create_board( board_data: KanbanBoardCreate, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Create a new kanban board for a channel""" # Check if channel exists and user has access channel = session.get(Channel, board_data.channel_id) if not channel: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Channel not found" ) # Check if user has access to the channel's department user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied to this channel" ) # Check if board already exists for this channel existing_board = session.exec( select(KanbanBoard).where(KanbanBoard.channel_id == board_data.channel_id) ).first() if existing_board: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Board already exists for this channel" ) new_board = KanbanBoard( channel_id=board_data.channel_id, name=board_data.name ) session.add(new_board) session.commit() session.refresh(new_board) return new_board @router.get("/boards/{id}", response_model=KanbanBoardWithColumns) def get_board_by_id_or_channel( id: int, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Get kanban board by ID or by channel ID""" # First try to get board directly by ID board = session.get(KanbanBoard, id) if board: # Check access via channel channel = session.get(Channel, board.channel_id) if not channel: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Board's associated channel not found" ) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied to this board" ) else: # If not found as board ID, try as channel ID channel = session.get(Channel, id) if not channel: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Board or channel not found" ) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied to this channel" ) # Get board for this channel board = session.exec( select(KanbanBoard).where(KanbanBoard.channel_id == id) ).first() if not board: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="No kanban board found for this channel" ) # Load columns with cards columns = session.exec( select(KanbanColumn) .where(KanbanColumn.board_id == board.id) .order_by(KanbanColumn.position) ).all() board_data = KanbanBoardWithColumns( id=board.id, channel_id=board.channel_id, name=board.name, created_at=board.created_at, updated_at=board.updated_at, columns=[] ) for column in columns: cards = session.exec( select(KanbanCard) .where(KanbanCard.column_id == column.id) .where(KanbanCard.is_archived == False) .order_by(KanbanCard.position) ).all() column_data = KanbanColumnWithCards( id=column.id, board_id=column.board_id, name=column.name, position=column.position, color=column.color, created_at=column.created_at, updated_at=column.updated_at, cards=[] ) for card in cards: # Load assignee data explicitly if card.assignee_id: card.assignee = session.get(User, card.assignee_id) # Calculate counts attachments_count = session.exec( select(func.count(KanbanCardAttachment.id)) .where(KanbanCardAttachment.card_id == card.id) ).one() checklists_count = session.exec( select(func.count(KanbanChecklist.id)) .where(KanbanChecklist.card_id == card.id) ).one() comments_count = session.exec( select(func.count(KanbanCardComment.id)) .where(KanbanCardComment.card_id == card.id) ).one() card_response = KanbanCardResponse.from_orm(card) card_response.attachments_count = attachments_count card_response.checklists_count = checklists_count card_response.comments_count = comments_count column_data.cards.append(card_response) board_data.columns.append(column_data) return board_data @router.put("/boards/{board_id}", response_model=KanbanBoardResponse) def update_board( board_id: int, board_data: KanbanBoardUpdate, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Update a kanban board""" board = session.get(KanbanBoard, board_id) if not board: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Board not found" ) # Check access via channel channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) update_data = board_data.dict(exclude_unset=True) for field, value in update_data.items(): setattr(board, field, value) session.commit() session.refresh(board) return board @router.get("/boards/by-id/{board_id}", response_model=KanbanBoardWithColumns) def get_board_by_id( board_id: int, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Get kanban board by board ID""" board = session.get(KanbanBoard, board_id) if not board: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Board not found" ) # Check access via channel channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied to this board" ) # Load columns with cards columns = session.exec( select(KanbanColumn) .where(KanbanColumn.board_id == board.id) .order_by(KanbanColumn.position) ).all() board_data = KanbanBoardWithColumns.from_orm(board) board_data.columns = [] for column in columns: cards = session.exec( select(KanbanCard) .where(KanbanCard.column_id == column.id) .where(KanbanCard.is_archived == False) .order_by(KanbanCard.position) ).all() # Load assignee data for all cards for card in cards: if card.assignee_id: card.assignee = session.get(User, card.assignee_id) column_data = KanbanColumnWithCards.from_orm(column) column_data.cards = [KanbanCardResponse.from_orm(card) for card in cards] board_data.columns.append(column_data) return board_data # Column endpoints @router.post("/columns", response_model=KanbanColumnResponse, status_code=status.HTTP_201_CREATED) def create_column( column_data: KanbanColumnCreate, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Create a new kanban column""" # Check if board exists and user has access board = session.get(KanbanBoard, column_data.board_id) if not board: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Board not found" ) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) new_column = KanbanColumn( board_id=column_data.board_id, name=column_data.name, position=column_data.position, color=column_data.color ) session.add(new_column) session.commit() session.refresh(new_column) return new_column @router.put("/columns/{column_id}", response_model=KanbanColumnResponse) def update_column( column_id: int, column_data: KanbanColumnUpdate, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Update a kanban column""" column = session.get(KanbanColumn, column_id) if not column: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Column not found" ) # Check access via board and channel board = session.get(KanbanBoard, column.board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) update_data = column_data.dict(exclude_unset=True) for field, value in update_data.items(): setattr(column, field, value) session.commit() session.refresh(column) return column @router.delete("/columns/{column_id}") def delete_column( column_id: int, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Delete a kanban column""" column = session.get(KanbanColumn, column_id) if not column: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Column not found" ) # Check access via board and channel board = session.get(KanbanBoard, column.board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) session.delete(column) session.commit() return {"message": "Column deleted successfully"} # Card endpoints @router.post("/cards", response_model=KanbanCardResponse, status_code=status.HTTP_201_CREATED) def create_card( card_data: KanbanCardCreate, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Create a new kanban card""" # Check if column exists and user has access column = session.get(KanbanColumn, card_data.column_id) if not column: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Column not found" ) board = session.get(KanbanBoard, column.board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) new_card = KanbanCard( column_id=card_data.column_id, title=card_data.title, description=card_data.description, assignee_id=card_data.assignee_id, position=card_data.position, due_date=card_data.due_date, priority=card_data.priority, labels=card_data.labels ) session.add(new_card) session.commit() session.refresh(new_card) # Log card creation log_card_activity( session, new_card.id, current_user.id, 'created', None, None, new_card.title ) return new_card @router.put("/cards/{card_id}", response_model=KanbanCardResponse) def update_card( card_id: int, card_data: KanbanCardUpdate, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Update a kanban card""" card = session.get(KanbanCard, card_id) if not card: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Card not found" ) # Check access via column, board and channel column = session.get(KanbanColumn, card.column_id) board = session.get(KanbanBoard, column.board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) update_data = card_data.dict(exclude_unset=True) # Log each field change for field, value in update_data.items(): old_value = getattr(card, field, None) if old_value != value: log_card_activity( session, card_id, current_user.id, 'updated', field, old_value, value ) setattr(card, field, value) session.commit() session.refresh(card) return card @router.delete("/cards/{card_id}") def delete_card( card_id: int, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Archive a kanban card (soft delete)""" card = session.get(KanbanCard, card_id) if not card: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Card not found" ) # Check access via column, board and channel column = session.get(KanbanColumn, card.column_id) board = session.get(KanbanBoard, column.board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) # Archive the card instead of deleting card.is_archived = True # Log the archive action log_card_activity( session, card_id, current_user.id, 'archived', None, None, card.title ) session.commit() return {"message": "Card archived successfully"} @router.put("/cards/{card_id}/move") def move_card( card_id: int, target_column_id: int, new_position: int, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Move a card to a different column and/or position""" card = session.get(KanbanCard, card_id) if not card: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Card not found" ) target_column = session.get(KanbanColumn, target_column_id) if not target_column: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Target column not found" ) # Check access for both source and target source_column = session.get(KanbanColumn, card.column_id) source_board = session.get(KanbanBoard, source_column.board_id) target_board = session.get(KanbanBoard, target_column.board_id) if source_board.id != target_board.id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot move cards between different boards" ) channel = session.get(Channel, source_board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) # Update card position old_column_id = card.column_id old_position = card.position card.column_id = target_column_id card.position = new_position # Log the move if column changed if old_column_id != target_column_id: old_column = session.get(KanbanColumn, old_column_id) new_column = session.get(KanbanColumn, target_column_id) log_card_activity( session, card_id, current_user.id, 'moved', 'column', old_column.name if old_column else 'Unknown', new_column.name if new_column else 'Unknown' ) session.commit() session.refresh(card) return {"message": "Card moved successfully"} @router.get("/boards/{board_id}/archived-cards", response_model=List[KanbanCardExtendedResponse]) def get_archived_cards( board_id: int, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Get all archived cards for a board""" board = session.get(KanbanBoard, board_id) if not board: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Board not found" ) # Check access via board -> channel -> department channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) # Get all columns of this board columns = session.exec( select(KanbanColumn).where(KanbanColumn.board_id == board_id) ).all() archived_cards = [] for column in columns: cards = session.exec( select(KanbanCard) .where(KanbanCard.column_id == column.id) .where(KanbanCard.is_archived == True) .order_by(KanbanCard.updated_at.desc()) ).all() for card in cards: # Load assignee data if card.assignee_id: card.assignee = session.get(User, card.assignee_id) # Add column name to card response card_dict = KanbanCardExtendedResponse.from_orm(card).dict() card_dict['column_name'] = column.name archived_cards.append(KanbanCardExtendedResponse(**card_dict)) return archived_cards # Checklist endpoints @router.post("/checklists", response_model=KanbanChecklistResponse, status_code=status.HTTP_201_CREATED) def create_checklist( checklist_data: KanbanChecklistCreate, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Create a new checklist for a card""" # Check if card exists and user has access card = session.get(KanbanCard, checklist_data.card_id) if not card: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Card not found" ) # Check access via board -> channel -> department board = session.get(KanbanBoard, card.column.board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) new_checklist = KanbanChecklist( card_id=checklist_data.card_id, title=checklist_data.title, position=checklist_data.position ) session.add(new_checklist) session.commit() session.refresh(new_checklist) return new_checklist @router.get("/checklists/{checklist_id}", response_model=KanbanChecklistWithItems) def get_checklist( checklist_id: int, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Get a checklist with its items""" checklist = session.get(KanbanChecklist, checklist_id) if not checklist: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Checklist not found" ) # Check access via card -> board -> channel -> department board = session.get(KanbanBoard, checklist.card.column.board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) return checklist @router.put("/checklists/{checklist_id}", response_model=KanbanChecklistResponse) def update_checklist( checklist_id: int, checklist_data: KanbanChecklistUpdate, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Update a checklist""" checklist = session.get(KanbanChecklist, checklist_id) if not checklist: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Checklist not found" ) # Check access board = session.get(KanbanBoard, checklist.card.column.board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) # Update fields if checklist_data.title is not None: checklist.title = checklist_data.title if checklist_data.position is not None: checklist.position = checklist_data.position session.commit() session.refresh(checklist) return checklist @router.delete("/checklists/{checklist_id}") def delete_checklist( checklist_id: int, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Delete a checklist""" checklist = session.get(KanbanChecklist, checklist_id) if not checklist: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Checklist not found" ) # Check access board = session.get(KanbanBoard, checklist.card.column.board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) session.delete(checklist) session.commit() return {"message": "Checklist deleted successfully"} # Checklist Item endpoints @router.post("/checklist-items", response_model=KanbanChecklistItemResponse, status_code=status.HTTP_201_CREATED) def create_checklist_item( item_data: KanbanChecklistItemCreate, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Create a new checklist item""" # Check if checklist exists and user has access checklist = session.get(KanbanChecklist, item_data.checklist_id) if not checklist: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Checklist not found" ) # Check access via checklist -> card -> board -> channel -> department board = session.get(KanbanBoard, checklist.card.column.board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) new_item = KanbanChecklistItem( checklist_id=item_data.checklist_id, title=item_data.title, is_completed=item_data.is_completed, position=item_data.position ) session.add(new_item) session.commit() session.refresh(new_item) return new_item @router.put("/checklist-items/{item_id}", response_model=KanbanChecklistItemResponse) def update_checklist_item( item_id: int, item_data: KanbanChecklistItemUpdate, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Update a checklist item""" item = session.get(KanbanChecklistItem, item_id) if not item: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Checklist item not found" ) # Check access via item -> checklist -> card -> board -> channel -> department board = session.get(KanbanBoard, item.checklist.card.column.board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) # Update fields if item_data.title is not None: item.title = item_data.title if item_data.is_completed is not None: item.is_completed = item_data.is_completed if item_data.position is not None: item.position = item_data.position session.commit() session.refresh(item) return item @router.delete("/checklist-items/{item_id}") def delete_checklist_item( item_id: int, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Delete a checklist item""" item = session.get(KanbanChecklistItem, item_id) if not item: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Checklist item not found" ) # Check access board = session.get(KanbanBoard, item.checklist.card.column.board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) session.delete(item) session.commit() return {"message": "Checklist item deleted successfully"} @router.get("/cards/{card_id}/checklists", response_model=List[KanbanChecklistWithItems]) def get_card_checklists( card_id: int, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Get all checklists for a specific card""" # Check if card exists and user has access card = session.get(KanbanCard, card_id) if not card: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Card not found" ) # Check access via board -> channel -> department board = session.get(KanbanBoard, card.column.board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) # Get all checklists for the card with their items checklists = session.exec( select(KanbanChecklist) .where(KanbanChecklist.card_id == card_id) .order_by(KanbanChecklist.position) ).all() result = [] for checklist in checklists: items = session.exec( select(KanbanChecklistItem) .where(KanbanChecklistItem.checklist_id == checklist.id) .order_by(KanbanChecklistItem.position) ).all() checklist_data = KanbanChecklistWithItems.from_orm(checklist) checklist_data.items = [KanbanChecklistItemResponse.from_orm(item) for item in items] result.append(checklist_data) return result # Comment endpoints @router.post("/cards/{card_id}/comments", response_model=KanbanCardCommentResponse, status_code=status.HTTP_201_CREATED) def create_card_comment( card_id: int, comment_data: KanbanCardCommentCreate, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Create a comment on a kanban card""" # Check if card exists and user has access card = session.get(KanbanCard, card_id) if not card: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Card not found" ) # Check access via board -> channel -> department board = session.get(KanbanBoard, card.column.board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) new_comment = KanbanCardComment( card_id=card_id, user_id=current_user.id, content=comment_data.content ) session.add(new_comment) session.commit() session.refresh(new_comment) return KanbanCardCommentResponse.from_orm(new_comment) @router.get("/cards/{card_id}/comments", response_model=List[KanbanCardCommentResponse]) def get_card_comments( card_id: int, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Get all comments for a kanban card""" # Check if card exists and user has access card = session.get(KanbanCard, card_id) if not card: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Card not found" ) # Check access via board -> channel -> department board = session.get(KanbanBoard, card.column.board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) comments = session.exec( select(KanbanCardComment) .where(KanbanCardComment.card_id == card_id) .order_by(KanbanCardComment.created_at) ).all() return [KanbanCardCommentResponse.from_orm(comment) for comment in comments] @router.get("/cards/{card_id}/activity") def get_card_activity( card_id: int, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Get activity log for a kanban card""" # Check if card exists and user has access card = session.get(KanbanCard, card_id) if not card: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Card not found" ) # Check access via board -> channel -> department board = session.get(KanbanBoard, card.column.board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) activities = session.exec( select(KanbanCardActivityLog) .where(KanbanCardActivityLog.card_id == card_id) .order_by(KanbanCardActivityLog.created_at.desc()) ).all() # Format response with user information result = [] for activity in activities: user = session.get(User, activity.user_id) result.append({ 'id': activity.id, 'action': activity.action, 'field_name': activity.field_name, 'old_value': activity.old_value, 'new_value': activity.new_value, 'created_at': activity.created_at, 'user': { 'id': user.id, 'username': user.username, 'full_name': user.full_name } if user else None }) return result @router.put("/comments/{comment_id}", response_model=KanbanCardCommentResponse) def update_card_comment( comment_id: int, comment_data: KanbanCardCommentUpdate, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Update a comment (only by the author)""" comment = session.get(KanbanCardComment, comment_id) if not comment: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Comment not found" ) # Only the author can update their comment if comment.user_id != current_user.id and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Can only update your own comments" ) if comment_data.content is not None: comment.content = comment_data.content comment.updated_at = datetime.utcnow() session.add(comment) session.commit() session.refresh(comment) return KanbanCardCommentResponse.from_orm(comment) @router.delete("/comments/{comment_id}") def delete_card_comment( comment_id: int, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Delete a comment (by author or admin)""" comment = session.get(KanbanCardComment, comment_id) if not comment: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Comment not found" ) # Only the author or admin can delete if comment.user_id != current_user.id and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Can only delete your own comments" ) session.delete(comment) session.commit() return {"message": "Comment deleted successfully"} # Attachment endpoints @router.post("/cards/{card_id}/attachments", response_model=KanbanCardAttachmentResponse, status_code=status.HTTP_201_CREATED) def upload_card_attachment( card_id: int, file: UploadFile = File(...), session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Upload a file attachment to a kanban card""" # Check if card exists and user has access card = session.get(KanbanCard, card_id) if not card: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Card not found" ) # Check access via board -> channel -> department board = session.get(KanbanBoard, card.column.board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) # Create uploads directory if it doesn't exist upload_dir = Path(__file__).parent.parent.parent / "uploads" / "kanban" upload_dir.mkdir(parents=True, exist_ok=True) # Generate unique filename file_extension = Path(file.filename).suffix unique_filename = f"{datetime.utcnow().timestamp()}_{current_user.id}_{file.filename}" file_path = upload_dir / unique_filename # Save file try: with open(file_path, "wb") as buffer: content = file.file.read() buffer.write(content) except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to save file: {str(e)}" ) # Create attachment record attachment = KanbanCardAttachment( card_id=card_id, filename=unique_filename, original_filename=file.filename, mime_type=file.content_type or "application/octet-stream", file_size=len(content), file_path=str(file_path.relative_to(Path(__file__).parent.parent.parent)), uploader_id=current_user.id ) session.add(attachment) session.commit() session.refresh(attachment) return KanbanCardAttachmentResponse.from_orm(attachment) @router.get("/cards/{card_id}/attachments", response_model=List[KanbanCardAttachmentResponse]) def get_card_attachments( card_id: int, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Get all attachments for a kanban card""" # Check if card exists and user has access card = session.get(KanbanCard, card_id) if not card: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Card not found" ) # Check access via board -> channel -> department board = session.get(KanbanBoard, card.column.board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) attachments = session.exec( select(KanbanCardAttachment) .where(KanbanCardAttachment.card_id == card_id) .order_by(KanbanCardAttachment.uploaded_at) ).all() return [KanbanCardAttachmentResponse.from_orm(att) for att in attachments] @router.delete("/attachments/{attachment_id}") def delete_card_attachment( attachment_id: int, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Delete an attachment (by uploader or admin)""" attachment = session.get(KanbanCardAttachment, attachment_id) if not attachment: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Attachment not found" ) # Only the uploader or admin can delete if attachment.uploader_id != current_user.id and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Can only delete your own attachments" ) # Delete file from disk try: file_path = Path(__file__).parent.parent.parent / attachment.file_path if file_path.exists(): file_path.unlink() except Exception as e: # Log error but don't fail the operation print(f"Failed to delete file {attachment.file_path}: {e}") session.delete(attachment) session.commit() return {"message": "Attachment deleted successfully"} @router.get("/attachments/{attachment_id}/download") def download_attachment( attachment_id: int, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Download a file attachment""" from fastapi.responses import FileResponse attachment = session.get(KanbanCardAttachment, attachment_id) if not attachment: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Attachment not found" ) # Check access via card -> board -> channel -> department card = session.get(KanbanCard, attachment.card_id) if not card: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Card not found" ) board = session.get(KanbanBoard, card.column.board_id) if not board: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Board not found" ) channel = session.get(Channel, board.channel_id) if not channel: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Channel not found" ) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) # Get file path file_path = Path(__file__).parent.parent.parent / attachment.file_path if not file_path.exists(): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="File not found on disk" ) return FileResponse( path=file_path, filename=attachment.original_filename, media_type=attachment.mime_type ) # Time tracking endpoints @router.post("/cards/{card_id}/time/start", response_model=KanbanTimeEntryResponse, status_code=status.HTTP_201_CREATED) def start_time_tracking( card_id: int, time_data: KanbanTimeEntryCreate, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Start time tracking for a kanban card""" # Check if card exists and user has access card = session.get(KanbanCard, card_id) if not card: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Card not found" ) # Check access via board -> channel -> department board = session.get(KanbanBoard, card.column.board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) # Check if user already has a running timer for this card running_entry = session.exec( select(KanbanTimeEntry) .where( KanbanTimeEntry.card_id == card_id, KanbanTimeEntry.user_id == current_user.id, KanbanTimeEntry.is_running == True ) ).first() if running_entry: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Time tracking already running for this card" ) new_entry = KanbanTimeEntry( card_id=card_id, user_id=current_user.id, description=time_data.description, start_time=datetime.utcnow(), is_running=True ) session.add(new_entry) session.commit() session.refresh(new_entry) return KanbanTimeEntryResponse.from_orm(new_entry) @router.put("/time/{entry_id}/stop", response_model=KanbanTimeEntryResponse) def stop_time_tracking( entry_id: int, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Stop time tracking for a kanban card""" entry = session.get(KanbanTimeEntry, entry_id) if not entry: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Time entry not found" ) # Only the user who started tracking can stop it if entry.user_id != current_user.id and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Can only stop your own time tracking" ) if not entry.is_running: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Time tracking is not running" ) end_time = datetime.utcnow() duration = int((end_time - entry.start_time).total_seconds() / 60) # Duration in minutes entry.end_time = end_time entry.duration_minutes = duration entry.is_running = False session.add(entry) session.commit() session.refresh(entry) return KanbanTimeEntryResponse.from_orm(entry) @router.get("/cards/{card_id}/time", response_model=List[KanbanTimeEntryResponse]) def get_card_time_entries( card_id: int, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Get all time entries for a kanban card""" # Check if card exists and user has access card = session.get(KanbanCard, card_id) if not card: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Card not found" ) # Check access via board -> channel -> department board = session.get(KanbanBoard, card.column.board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) entries = session.exec( select(KanbanTimeEntry) .where(KanbanTimeEntry.card_id == card_id) .order_by(KanbanTimeEntry.start_time.desc()) ).all() return [KanbanTimeEntryResponse.from_orm(entry) for entry in entries] # Custom field endpoints @router.post("/boards/{board_id}/custom-fields", response_model=KanbanCustomFieldResponse, status_code=status.HTTP_201_CREATED) def create_custom_field( board_id: int, field_data: KanbanCustomFieldCreate, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Create a custom field for a kanban board""" # Check if board exists and user has access board = session.get(KanbanBoard, board_id) if not board: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Board not found" ) # Check access via channel -> department channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) new_field = KanbanCustomField( board_id=board_id, name=field_data.name, field_type=field_data.field_type, options=field_data.options, is_required=field_data.is_required, position=field_data.position ) session.add(new_field) session.commit() session.refresh(new_field) return KanbanCustomFieldResponse.from_orm(new_field) @router.get("/boards/{board_id}/custom-fields", response_model=List[KanbanCustomFieldResponse]) def get_board_custom_fields( board_id: int, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Get all custom fields for a kanban board""" # Check if board exists and user has access board = session.get(KanbanBoard, board_id) if not board: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Board not found" ) # Check access via channel -> department channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) fields = session.exec( select(KanbanCustomField) .where(KanbanCustomField.board_id == board_id) .order_by(KanbanCustomField.position) ).all() return [KanbanCustomFieldResponse.from_orm(field) for field in fields] @router.put("/custom-fields/{field_id}", response_model=KanbanCustomFieldResponse) def update_custom_field( field_id: int, field_data: KanbanCustomFieldUpdate, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Update a custom field""" field = session.get(KanbanCustomField, field_id) if not field: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Custom field not found" ) # Check access via board -> channel -> department board = session.get(KanbanBoard, field.board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) # Update fields for attr, value in field_data.dict(exclude_unset=True).items(): if value is not None: setattr(field, attr, value) session.add(field) session.commit() session.refresh(field) return KanbanCustomFieldResponse.from_orm(field) @router.delete("/custom-fields/{field_id}") def delete_custom_field( field_id: int, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Delete a custom field""" field = session.get(KanbanCustomField, field_id) if not field: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Custom field not found" ) # Check access via board -> channel -> department board = session.get(KanbanBoard, field.board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) session.delete(field) session.commit() return {"message": "Custom field deleted successfully"} # Custom field value endpoints @router.put("/cards/{card_id}/custom-fields/{field_id}", response_model=KanbanCustomFieldValueResponse) def set_custom_field_value( card_id: int, field_id: int, value_data: KanbanCustomFieldValueCreate, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Set or update a custom field value for a card""" # Check if card and field exist card = session.get(KanbanCard, card_id) field = session.get(KanbanCustomField, field_id) if not card: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Card not found" ) if not field: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Custom field not found" ) # Check if field belongs to the card's board board = session.get(KanbanBoard, card.column.board_id) if field.board_id != board.id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Field does not belong to this board" ) # Check access via channel -> department channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) # Find existing value or create new one existing_value = session.exec( select(KanbanCustomFieldValue) .where( KanbanCustomFieldValue.field_id == field_id, KanbanCustomFieldValue.card_id == card_id ) ).first() if existing_value: existing_value.value = value_data.value existing_value.updated_at = datetime.utcnow() session.add(existing_value) session.commit() session.refresh(existing_value) return KanbanCustomFieldValueResponse.from_orm(existing_value) else: new_value = KanbanCustomFieldValue( field_id=field_id, card_id=card_id, value=value_data.value ) session.add(new_value) session.commit() session.refresh(new_value) return KanbanCustomFieldValueResponse.from_orm(new_value) @router.get("/cards/{card_id}/custom-fields", response_model=List[KanbanCustomFieldValueResponse]) def get_card_custom_field_values( card_id: int, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Get all custom field values for a card""" # Check if card exists and user has access card = session.get(KanbanCard, card_id) if not card: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Card not found" ) # Check access via board -> channel -> department board = session.get(KanbanBoard, card.column.board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) values = session.exec( select(KanbanCustomFieldValue) .where(KanbanCustomFieldValue.card_id == card_id) ).all() return [KanbanCustomFieldValueResponse.from_orm(value) for value in values] # Template endpoints @router.post("/boards/{board_id}/templates", response_model=KanbanCardTemplateResponse, status_code=status.HTTP_201_CREATED) def create_card_template( board_id: int, template_data: KanbanCardTemplateCreate, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Create a card template for a kanban board""" # Check if board exists and user has access board = session.get(KanbanBoard, board_id) if not board: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Board not found" ) # Check access via channel -> department channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) new_template = KanbanCardTemplate( board_id=board_id, name=template_data.name, description=template_data.description, template_data=template_data.template_data, is_default=template_data.is_default ) session.add(new_template) session.commit() session.refresh(new_template) return KanbanCardTemplateResponse.from_orm(new_template) @router.get("/boards/{board_id}/templates", response_model=List[KanbanCardTemplateResponse]) def get_board_templates( board_id: int, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Get all templates for a kanban board""" # Check if board exists and user has access board = session.get(KanbanBoard, board_id) if not board: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Board not found" ) # Check access via channel -> department channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) templates = session.exec( select(KanbanCardTemplate) .where(KanbanCardTemplate.board_id == board_id) .order_by(KanbanCardTemplate.name) ).all() return [KanbanCardTemplateResponse.from_orm(template) for template in templates] @router.post("/cards/from-template/{template_id}", response_model=KanbanCardResponse, status_code=status.HTTP_201_CREATED) def create_card_from_template( template_id: int, column_id: int, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Create a card from a template""" # Check if template and column exist template = session.get(KanbanCardTemplate, template_id) column = session.get(KanbanColumn, column_id) if not template: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Template not found" ) if not column: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Column not found" ) # Check access via board -> channel -> department board = session.get(KanbanBoard, template.board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) # Parse template data (assuming it's JSON) import json try: template_dict = json.loads(template.template_data) except json.JSONDecodeError: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid template data" ) # Create card from template new_card = KanbanCard( column_id=column_id, title=template_dict.get('title', template.name), description=template_dict.get('description', ''), priority=template_dict.get('priority', 'medium'), labels=template_dict.get('labels', None), estimated_time=template_dict.get('estimated_time'), assignee_id=template_dict.get('assignee_id') ) session.add(new_card) session.commit() session.refresh(new_card) return KanbanCardResponse.from_orm(new_card) # Bulk operations endpoints @router.post("/cards/bulk/move") def bulk_move_cards( move_data: dict, # {"card_ids": [1,2,3], "column_id": 5, "position": 0} session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Move multiple cards to a new column/position""" card_ids = move_data.get('card_ids', []) column_id = move_data.get('column_id') position = move_data.get('position', 0) if not card_ids or not column_id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="card_ids and column_id are required" ) # Check if target column exists column = session.get(KanbanColumn, column_id) if not column: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Target column not found" ) # Check access via board -> channel -> department board = session.get(KanbanBoard, column.board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) # Get and update cards cards = session.exec( select(KanbanCard).where(KanbanCard.id.in_(card_ids)) ).all() updated_cards = [] for i, card in enumerate(cards): card.column_id = column_id card.position = position + i updated_cards.append(card) session.add_all(updated_cards) session.commit() return {"message": f"Moved {len(updated_cards)} cards successfully"} @router.delete("/cards/bulk") def bulk_delete_cards( delete_data: dict, # {"card_ids": [1,2,3]} session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Delete multiple cards""" card_ids = delete_data.get('card_ids', []) if not card_ids: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="card_ids are required" ) # Get cards and check access cards = session.exec( select(KanbanCard).where(KanbanCard.id.in_(card_ids)) ).all() if not cards: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="No cards found" ) # Check access for all cards (they should all be from the same board) board_id = cards[0].column.board_id board = session.get(KanbanBoard, board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) # Delete cards (cascading will handle related data) for card in cards: session.delete(card) session.commit() return {"message": f"Deleted {len(cards)} cards successfully"} # Search and filter endpoints @router.get("/boards/{board_id}/search") def search_cards( board_id: int, q: str = "", # Search query assignee_id: Optional[int] = None, priority: Optional[str] = None, labels: Optional[str] = None, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Search and filter cards in a board""" # Check if board exists and user has access board = session.get(KanbanBoard, board_id) if not board: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Board not found" ) # Check access via channel -> department channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) # Build query query = select(KanbanCard).where(KanbanCard.column.has(KanbanColumn.board_id == board_id)) # Apply filters if q: query = query.where( (KanbanCard.title.contains(q)) | (KanbanCard.description.contains(q)) ) if assignee_id: query = query.where(KanbanCard.assignee_id == assignee_id) if priority: query = query.where(KanbanCard.priority == priority) if labels: # Search for cards that contain any of the specified labels label_conditions = [] for label in labels.split(','): label_conditions.append(KanbanCard.labels.contains(label.strip())) if label_conditions: query = query.where(or_(*label_conditions)) cards = session.exec(query).all() return [KanbanCardResponse.from_orm(card) for card in cards] # Extended card endpoint with all features @router.get("/cards/{card_id}/extended", response_model=KanbanCardExtendedResponse) def get_card_extended( card_id: int, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Get a card with all extended features (comments, attachments, time entries, custom fields)""" # Check if card exists and user has access card = session.get(KanbanCard, card_id) if not card: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Card not found" ) # Check access via board -> channel -> department board = session.get(KanbanBoard, card.column.board_id) channel = session.get(Channel, board.channel_id) user_departments = [dept.id for dept in current_user.departments] if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) # Build extended response response = KanbanCardExtendedResponse.from_orm(card) # Load comments comments = session.exec( select(KanbanCardComment) .where(KanbanCardComment.card_id == card_id) .order_by(KanbanCardComment.created_at) ).all() response.comments = [KanbanCardCommentResponse.from_orm(comment) for comment in comments] # Load attachments attachments = session.exec( select(KanbanCardAttachment) .where(KanbanCardAttachment.card_id == card_id) .order_by(KanbanCardAttachment.uploaded_at) ).all() response.attachments = [KanbanCardAttachmentResponse.from_orm(att) for att in attachments] # Load time entries time_entries = session.exec( select(KanbanTimeEntry) .where(KanbanTimeEntry.card_id == card_id) .order_by(KanbanTimeEntry.start_time.desc()) ).all() response.time_entries = [KanbanTimeEntryResponse.from_orm(entry) for entry in time_entries] # Load custom field values custom_values = session.exec( select(KanbanCustomFieldValue) .where(KanbanCustomFieldValue.card_id == card_id) ).all() response.custom_field_values = [KanbanCustomFieldValueResponse.from_orm(value) for value in custom_values] # Load checklists with items checklists = session.exec( select(KanbanChecklist) .where(KanbanChecklist.card_id == card_id) .order_by(KanbanChecklist.position) ).all() checklist_data = [] for checklist in checklists: items = session.exec( select(KanbanChecklistItem) .where(KanbanChecklistItem.checklist_id == checklist.id) .order_by(KanbanChecklistItem.position) ).all() checklist_response = KanbanChecklistWithItems.from_orm(checklist) checklist_response.items = [KanbanChecklistItemResponse.from_orm(item) for item in items] checklist_data.append(checklist_response) response.checklists = checklist_data return response