diff --git a/apps/api/src/routers/trail.py b/apps/api/src/routers/trail.py index cb9b3d48..1dedc12a 100644 --- a/apps/api/src/routers/trail.py +++ b/apps/api/src/routers/trail.py @@ -10,6 +10,7 @@ from src.services.trail.trail import ( get_user_trails, get_user_trail_with_orgid, remove_course_from_trail, + remove_activity_from_trail, ) @@ -95,3 +96,16 @@ async def api_add_activity_to_trail( return await add_activity_to_trail( request, user, activity_uuid, db_session ) + + +@router.delete("/remove_activity/{activity_uuid}") +async def api_remove_activity_from_trail( + request: Request, + activity_uuid: str, + user=Depends(get_current_user), + db_session=Depends(get_db_session), +) -> TrailRead: + """ + Remove Activity from trail + """ + return await remove_activity_from_trail(request, user, activity_uuid, db_session) diff --git a/apps/api/src/services/trail/trail.py b/apps/api/src/services/trail/trail.py index 7daebdea..30a5bffc 100644 --- a/apps/api/src/services/trail/trail.py +++ b/apps/api/src/services/trail/trail.py @@ -282,6 +282,79 @@ async def add_activity_to_trail( return trail_read +async def remove_activity_from_trail( + request: Request, + user: PublicUser, + activity_uuid: str, + db_session: Session, +) -> TrailRead: + # Look for the activity + statement = select(Activity).where(Activity.activity_uuid == activity_uuid) + activity = db_session.exec(statement).first() + + if not activity: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found" + ) + + statement = select(Course).where(Course.id == activity.course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Course not found" + ) + + statement = select(Trail).where( + Trail.org_id == course.org_id, Trail.user_id == user.id + ) + trail = db_session.exec(statement).first() + + if not trail: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found" + ) + + # Delete the trail step for this activity + statement = select(TrailStep).where( + TrailStep.activity_id == activity.id, + TrailStep.user_id == user.id, + TrailStep.trail_id == trail.id + ) + trail_step = db_session.exec(statement).first() + + if trail_step: + db_session.delete(trail_step) + db_session.commit() + + # Get updated trail data + statement = select(TrailRun).where(TrailRun.trail_id == trail.id, TrailRun.user_id == user.id) + trail_runs = db_session.exec(statement).all() + + trail_runs = [ + TrailRunRead(**trail_run.__dict__, course={}, steps=[], course_total_steps=0) + for trail_run in trail_runs + ] + + for trail_run in trail_runs: + statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id, TrailStep.user_id == user.id) + trail_steps = db_session.exec(statement).all() + + trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps] + trail_run.steps = trail_steps + + for trail_step in trail_steps: + statement = select(Course).where(Course.id == trail_step.course_id) + course = db_session.exec(statement).first() + trail_step.data = dict(course=course) + + trail_read = TrailRead( + **trail.model_dump(), + runs=trail_runs, + ) + + return trail_read + async def add_course_to_trail( request: Request, diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx index 6ae554ff..b7f70ecc 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx @@ -4,7 +4,7 @@ import { getAPIUrl, getUriWithOrg } from '@services/config/config' import Canva from '@components/Objects/Activities/DynamicCanva/DynamicCanva' import VideoActivity from '@components/Objects/Activities/Video/Video' import { BookOpenCheck, Check, CheckCircle, ChevronDown, ChevronLeft, ChevronRight, FileText, Folder, List, Menu, MoreVertical, UserRoundPen, Video, Layers, ListFilter, ListTree, X, Edit2 } from 'lucide-react' -import { markActivityAsComplete } from '@services/courses/activity' +import { markActivityAsComplete, unmarkActivityAsComplete } from '@services/courses/activity' import DocumentPdfActivity from '@components/Objects/Activities/DocumentPdf/DocumentPdf' import ActivityIndicators from '@components/Pages/Courses/ActivityIndicators' import GeneralWrapperStyled from '@components/Objects/StyledElements/Wrappers/GeneralWrapper' @@ -28,6 +28,7 @@ import ConfirmationModal from '@components/Objects/StyledElements/ConfirmationMo import { useMediaQuery } from 'usehooks-ts' import PaidCourseActivityDisclaimer from '@components/Objects/Courses/CourseActions/PaidCourseActivityDisclaimer' import { useContributorStatus } from '../../../../../../../../hooks/useContributorStatus' +import ToolTip from '@components/Objects/StyledElements/Tooltip/Tooltip' interface ActivityClientProps { activityid: string @@ -282,6 +283,26 @@ export function MarkStatus(props: { } } + async function unmarkActivityAsCompleteFront() { + try { + setIsLoading(true); + const trail = await unmarkActivityAsComplete( + props.orgslug, + props.course.course_uuid, + props.activity.activity_uuid, + session.data?.tokens?.access_token + ); + + // Mutate the course data to trigger re-render + await mutate(`${getAPIUrl()}courses/${props.course.course_uuid}/meta`); + router.refresh(); + } catch (error) { + toast.error('Failed to unmark activity as complete'); + } finally { + setIsLoading(false); + } + } + const isActivityCompleted = () => { let run = props.course.trail.runs.find( (run: any) => run.course_id == props.course.id @@ -296,11 +317,33 @@ export function MarkStatus(props: { return ( <> {isActivityCompleted() ? ( -
- - - {' '} - Complete +
+
+ + + {' '} + Complete +
+ +
+ {isLoading ? ( +
+ + + + +
+ ) : ( + + )} +
+
) : (