feat: implement activity removal from trail and update UI for unmarking activities

This commit is contained in:
swve 2025-04-16 15:24:40 +02:00
parent b25505465b
commit 1350cb7354
4 changed files with 150 additions and 6 deletions

View file

@ -10,6 +10,7 @@ from src.services.trail.trail import (
get_user_trails, get_user_trails,
get_user_trail_with_orgid, get_user_trail_with_orgid,
remove_course_from_trail, 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( return await add_activity_to_trail(
request, user, activity_uuid, db_session 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)

View file

@ -282,6 +282,79 @@ async def add_activity_to_trail(
return trail_read 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( async def add_course_to_trail(
request: Request, request: Request,

View file

@ -4,7 +4,7 @@ import { getAPIUrl, getUriWithOrg } from '@services/config/config'
import Canva from '@components/Objects/Activities/DynamicCanva/DynamicCanva' import Canva from '@components/Objects/Activities/DynamicCanva/DynamicCanva'
import VideoActivity from '@components/Objects/Activities/Video/Video' 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 { 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 DocumentPdfActivity from '@components/Objects/Activities/DocumentPdf/DocumentPdf'
import ActivityIndicators from '@components/Pages/Courses/ActivityIndicators' import ActivityIndicators from '@components/Pages/Courses/ActivityIndicators'
import GeneralWrapperStyled from '@components/Objects/StyledElements/Wrappers/GeneralWrapper' 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 { useMediaQuery } from 'usehooks-ts'
import PaidCourseActivityDisclaimer from '@components/Objects/Courses/CourseActions/PaidCourseActivityDisclaimer' import PaidCourseActivityDisclaimer from '@components/Objects/Courses/CourseActions/PaidCourseActivityDisclaimer'
import { useContributorStatus } from '../../../../../../../../hooks/useContributorStatus' import { useContributorStatus } from '../../../../../../../../hooks/useContributorStatus'
import ToolTip from '@components/Objects/StyledElements/Tooltip/Tooltip'
interface ActivityClientProps { interface ActivityClientProps {
activityid: string 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 = () => { const isActivityCompleted = () => {
let run = props.course.trail.runs.find( let run = props.course.trail.runs.find(
(run: any) => run.course_id == props.course.id (run: any) => run.course_id == props.course.id
@ -296,11 +317,33 @@ export function MarkStatus(props: {
return ( return (
<> <>
{isActivityCompleted() ? ( {isActivityCompleted() ? (
<div className="bg-teal-600 rounded-full px-5 drop-shadow-md flex items-center space-x-2 p-2.5 text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out"> <div className="flex items-center space-x-2">
<i> <div className="bg-teal-600 rounded-full px-5 drop-shadow-md flex items-center space-x-2 p-2.5 text-white">
<Check size={17}></Check> <i>
</i>{' '} <Check size={17}></Check>
<i className="not-italic text-xs font-bold">Complete</i> </i>{' '}
<i className="not-italic text-xs font-bold">Complete</i>
</div>
<ToolTip
content="Unmark as complete"
side="top"
>
<div
className={`${isLoading ? 'opacity-75 cursor-not-allowed' : ''} bg-red-400 rounded-full p-2 drop-shadow-md flex items-center text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out`}
onClick={!isLoading ? unmarkActivityAsCompleteFront : undefined}
>
{isLoading ? (
<div className="animate-spin">
<svg className="w-4 h-4" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
) : (
<X size={17} />
)}
</div>
</ToolTip>
</div> </div>
) : ( ) : (
<div <div

View file

@ -36,3 +36,17 @@ export async function markActivityAsComplete(
const res = await errorHandling(result) const res = await errorHandling(result)
return res return res
} }
export async function unmarkActivityAsComplete(
org_slug: string,
course_uuid: string,
activity_uuid: string,
access_token: any
) {
const result: any = await fetch(
`${getAPIUrl()}trail/remove_activity/${activity_uuid}`,
RequestBodyWithAuthHeader('DELETE', null, null, access_token)
)
const res = await errorHandling(result)
return res
}