diff --git a/src/backend/app/models/enums.py b/src/backend/app/models/enums.py
index 2a3d3ddd..2282f5ca 100644
--- a/src/backend/app/models/enums.py
+++ b/src/backend/app/models/enums.py
@@ -161,6 +161,7 @@ class EventType(str, Enum):
- ``split`` -- Set the state *unlocked done* then generate additional subdivided task areas.
- ``assign`` -- For a requester user to assign a task to another user. Set the state *locked for mapping* passing in the required user id.
- ``comment`` -- Keep the state the same, but simply add a comment.
+ - ``unlock`` -- Unlock a task state by unlocking it if it's locked.
Note that ``task_id`` must be specified in the endpoint too.
"""
@@ -175,3 +176,4 @@ class EventType(str, Enum):
SPLIT = "split"
ASSIGN = "assign"
COMMENT = "comment"
+ UNLOCK = "unlock"
diff --git a/src/backend/app/projects/project_schemas.py b/src/backend/app/projects/project_schemas.py
index a292278f..c49f746d 100644
--- a/src/backend/app/projects/project_schemas.py
+++ b/src/backend/app/projects/project_schemas.py
@@ -283,18 +283,38 @@ async def all(
limit: int = 100,
):
"""
- Get all projects. Optionally filter by the project creator (user).
+ Get all projects, count total tasks and task states (ongoing, completed, etc.).
+ Optionally filter by the project creator (user).
"""
async with db.cursor(row_factory=dict_row) as cur:
await cur.execute(
"""
SELECT
- id, slug, name, description, per_task_instructions,
- ST_AsGeoJSON(outline)::jsonb AS outline,
- requires_approval_from_manager_for_locking
- FROM projects
- WHERE (author_id = COALESCE(%(user_id)s, author_id))
- ORDER BY created_at DESC
+ p.id, p.slug, p.name, p.description, p.per_task_instructions,
+ ST_AsGeoJSON(p.outline)::jsonb AS outline,
+ p.requires_approval_from_manager_for_locking,
+
+ -- Count total tasks for each project
+ COUNT(t.id) AS total_task_count,
+
+ -- Count based on the latest state of tasks
+ COUNT(CASE WHEN te.state = 'LOCKED_FOR_MAPPING' THEN 1 END) AS ongoing_task_count
+
+ FROM projects p
+ LEFT JOIN tasks t ON t.project_id = p.id
+ LEFT JOIN (
+ -- Get the latest event per task
+ SELECT DISTINCT ON (te.task_id)
+ te.task_id,
+ te.state,
+ te.created_at
+ FROM task_events te
+ ORDER BY te.task_id, te.created_at DESC
+ ) AS te ON te.task_id = t.id
+
+ WHERE (p.author_id = COALESCE(%(user_id)s, p.author_id))
+ GROUP BY p.id
+ ORDER BY p.created_at DESC
OFFSET %(skip)s
LIMIT %(limit)s
""",
@@ -417,9 +437,10 @@ class ProjectOut(BaseModel):
outline: Optional[Polygon | Feature | FeatureCollection]
no_fly_zones: Optional[Polygon | Feature | FeatureCollection | MultiPolygon] = None
requires_approval_from_manager_for_locking: bool
- task_count: int = 0
+ total_task_count: int = 0
tasks: Optional[list[TaskOut]] = []
image_url: Optional[str] = None
+ ongoing_task_count: Optional[int] = 0
@model_validator(mode="after")
def set_image_url(cls, values):
diff --git a/src/backend/app/tasks/task_logic.py b/src/backend/app/tasks/task_logic.py
index 68ec2210..c44c11b0 100644
--- a/src/backend/app/tasks/task_logic.py
+++ b/src/backend/app/tasks/task_logic.py
@@ -128,3 +128,41 @@ async def request_mapping(
)
result = await cur.fetchone()
return result
+
+
+async def get_task_state(
+ db: Connection, project_id: uuid.UUID, task_id: uuid.UUID
+) -> dict:
+ """
+ Retrieve the latest state of a task by querying the task_events table.
+
+ Args:
+ db (Connection): The database connection.
+ project_id (uuid.UUID): The project ID.
+ task_id (uuid.UUID): The task ID.
+
+ Returns:
+ dict: A dictionary containing the task's state and associated metadata.
+ """
+ try:
+ async with db.cursor(row_factory=dict_row) as cur:
+ await cur.execute(
+ """
+ SELECT state, user_id, created_at, comment
+ FROM task_events
+ WHERE project_id = %(project_id)s AND task_id = %(task_id)s
+ ORDER BY created_at DESC
+ LIMIT 1;
+ """,
+ {
+ "project_id": str(project_id),
+ "task_id": str(task_id),
+ },
+ )
+ result = await cur.fetchone()
+ return result
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"An error occurred while retrieving the task state: {str(e)}",
+ )
diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py
index c7323d46..890ad7fa 100644
--- a/src/backend/app/tasks/task_routes.py
+++ b/src/backend/app/tasks/task_routes.py
@@ -368,4 +368,36 @@ async def new_event(
State.UNFLYABLE_TASK,
)
+ case EventType.UNLOCK:
+ # Fetch the task state
+ current_task_state = await task_logic.get_task_state(
+ db, project_id, task_id
+ )
+
+ state = current_task_state.get("state")
+ locked_user_id = current_task_state.get("user_id")
+
+ # Determine error conditions
+ if state != State.LOCKED_FOR_MAPPING.name:
+ raise HTTPException(
+ status_code=400,
+ detail="Task state does not match expected state for unlock operation.",
+ )
+ if user_id != locked_user_id:
+ raise HTTPException(
+ status_code=403,
+ detail="You cannot unlock this task as it is locked by another user.",
+ )
+
+ # Proceed with unlocking the task
+ return await task_logic.update_task_state(
+ db,
+ project_id,
+ task_id,
+ user_id,
+ f"Task has been unlock by user {user_data.name}.",
+ State.LOCKED_FOR_MAPPING,
+ State.UNLOCKED_TO_MAP,
+ )
+
return True
diff --git a/src/backend/app/waypoints/waypoint_routes.py b/src/backend/app/waypoints/waypoint_routes.py
index 8fe23bb7..af795fb2 100644
--- a/src/backend/app/waypoints/waypoint_routes.py
+++ b/src/backend/app/waypoints/waypoint_routes.py
@@ -122,7 +122,6 @@ async def get_task_waypoint(
if download:
outfile = outfile = f"/tmp/{uuid.uuid4()}"
kmz_file = wpml.create_wpml(placemarks, outfile)
-
return FileResponse(
kmz_file, media_type="application/zip", filename="flight_plan.kmz"
)
diff --git a/src/frontend/src/assets/images/LandingPage/JamaicaFlyingLabs_Logo.png b/src/frontend/src/assets/images/LandingPage/JamaicaFlyingLabs_Logo.png
new file mode 100644
index 00000000..b4fd78e3
Binary files /dev/null and b/src/frontend/src/assets/images/LandingPage/JamaicaFlyingLabs_Logo.png differ
diff --git a/src/frontend/src/components/CreateProject/CreateprojectLayout/index.tsx b/src/frontend/src/components/CreateProject/CreateprojectLayout/index.tsx
index 00852b06..475a9a90 100644
--- a/src/frontend/src/components/CreateProject/CreateprojectLayout/index.tsx
+++ b/src/frontend/src/components/CreateProject/CreateprojectLayout/index.tsx
@@ -25,6 +25,7 @@ import {
import { convertGeojsonToFile } from '@Utils/convertLayerUtils';
import prepareFormData from '@Utils/prepareFormData';
import hasErrorBoundary from '@Utils/hasErrorBoundary';
+import { getFrontOverlap, getSideOverlap, gsdToAltitude } from '@Utils/index';
/**
* This function looks up the provided map of components to find and return
@@ -86,6 +87,9 @@ const CreateprojectLayout = () => {
const capturedProjectMap = useTypedSelector(
state => state.createproject.capturedProjectMap,
);
+ const imageMergeType = useTypedSelector(
+ state => state.createproject.imageMergeType,
+ );
const initialState: FieldValues = {
name: '',
@@ -214,13 +218,29 @@ const CreateprojectLayout = () => {
return;
}
+ // get altitude
+ const agl =
+ measurementType === 'gsd'
+ ? gsdToAltitude(data?.gsd_cm_px)
+ : data?.altitude_from_ground;
+
const refactoredData = {
...data,
is_terrain_follow: isTerrainFollow,
requires_approval_from_manager_for_locking:
requireApprovalFromManagerForLocking === 'required',
deadline_at: data?.deadline_at ? data?.deadline_at : null,
+ front_overlap:
+ imageMergeType === 'spacing'
+ ? getFrontOverlap(agl, data?.forward_spacing)
+ : data?.front_overlap,
+ side_overlap:
+ imageMergeType === 'spacing'
+ ? getSideOverlap(agl, data?.side_spacing)
+ : data?.side_overlap,
};
+ delete refactoredData?.forward_spacing;
+ delete refactoredData?.side_spacing;
// remove key
if (isNoflyzonePresent === 'no')
diff --git a/src/frontend/src/components/CreateProject/DescriptionContents/GenerateTask/index.tsx b/src/frontend/src/components/CreateProject/DescriptionContents/GenerateTask/index.tsx
index becbab36..b336b4ed 100644
--- a/src/frontend/src/components/CreateProject/DescriptionContents/GenerateTask/index.tsx
+++ b/src/frontend/src/components/CreateProject/DescriptionContents/GenerateTask/index.tsx
@@ -1,3 +1,5 @@
+import { taskGenerationGuidelines } from '@Constants/createProject';
+
export default function GenerateTask() {
return (
@@ -6,6 +8,19 @@ export default function GenerateTask() {
Split the task into smaller chunks based on the given dimensions to
ensure more efficient and precise data collection and analysis.
+
+