diff --git a/backend/api/models/Training.py b/backend/api/models/Training.py index e9d7c293..9a990de0 100644 --- a/backend/api/models/Training.py +++ b/backend/api/models/Training.py @@ -27,10 +27,12 @@ class Training(Document, Mixin): partner_id = StringField(required=False) mentor_id = ListField(StringField(), required=False) mentee_id = ListField(StringField(), required=False) + sort_order = IntField(required=False, default=0) def __repr__(self): return f"""""" + \n date_submitted: {self.date_submitted} + \n sort_order: {self.sort_order}>""" \ No newline at end of file diff --git a/backend/api/views/training.py b/backend/api/views/training.py index b9246ec2..85910b4b 100644 --- a/backend/api/views/training.py +++ b/backend/api/views/training.py @@ -169,6 +169,71 @@ def get_trainings(role): return create_response(data={"trainings": result}) +@training.route("/update_multiple", methods=["PATCH"]) +def update_multiple_trainings(): + data = request.json.get("trainings", []) + if not data: + return create_response(status=400, message="No trainings provided for update") + + updated_trainings = [] + failed_updates = [] + + for training in data: + training_id = training.get("id") + update_data = training.get("updated_data", {}) + + if not training_id: + failed_updates.append({"error": "Training ID is required"}) + continue + + try: + training_id = ObjectId(training_id) + except Exception as e: + failed_updates.append({"error": f"Invalid ID format: {str(e)}"}) + continue + + train = Training.objects(id=training_id).first() + if not train: + failed_updates.append({"error": f"Training with ID {training_id} not found"}) + continue + + train_data = train.to_mongo().to_dict() + + # Exclude `_id` and `sort_order` for comparison + train_data.pop("_id", None) + existing_sort_order = train_data.pop("sort_order", None) + updated_sort_order = update_data.get("sort_order") + + # Compare all fields except `sort_order` + other_fields_match = all( + train_data.get(key) == value + for key, value in update_data.items() + if key != "sort_order" + ) + + if not other_fields_match: + failed_updates.append({ + "error": f"Only sort_order can be updated. Mismatched fields for ID {training_id}" + }) + continue + + # Update sort_order if it's different + if updated_sort_order != existing_sort_order: + train.update(sort_order=updated_sort_order) + updated_trainings.append(train.reload()) + else: + failed_updates.append({ + "error": f"No changes in sort_order for ID {training_id}" + }) + + result = { + "updated_trainings": [json.loads(t.to_json()) for t in updated_trainings], + "failed_updates": failed_updates, + } + + return create_response(data=result) + + @training.route("/", methods=["DELETE"]) @admin_only diff --git a/frontend/package.json b/frontend/package.json index 91376dbc..ff3313ae 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,6 +4,9 @@ "private": true, "dependencies": { "@ant-design/icons": "^4.8.0", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", "@emotion/css": "^11.11.2", "@jitsi/react-sdk": "^1.4.0", "@reduxjs/toolkit": "^1.9.3", diff --git a/frontend/src/components/TrainingList.js b/frontend/src/components/TrainingList.js index f0eae06a..dcba22b1 100644 --- a/frontend/src/components/TrainingList.js +++ b/frontend/src/components/TrainingList.js @@ -203,9 +203,13 @@ const TrainingList = (props) => { } else { hub_user_id = user._id.$oid; } - setTrainingData(trains.filter((x) => x.hub_id == hub_user_id)); + setTrainingData( + trains + .sort((a, b) => a.sort_order - b.sort_order) + .filter((x) => x.hub_id == hub_user_id) + ); } else { - setTrainingData(trains); + setTrainingData(trains.sort((a, b) => a.sort_order - b.sort_order)); } setLoading(false); setFlag(!flag); diff --git a/frontend/src/components/pages/AdminTraining.js b/frontend/src/components/pages/AdminTraining.js index c6c4d58b..518dfad4 100644 --- a/frontend/src/components/pages/AdminTraining.js +++ b/frontend/src/components/pages/AdminTraining.js @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from "react"; +import React, { useContext, useEffect, useMemo, useState } from "react"; import { deleteTrainbyId, downloadBlob, @@ -9,6 +9,7 @@ import { fetchAccounts, fetchPartners, newTrainCreate, + updateTrainings, } from "utils/api"; import { ACCOUNT_TYPE, I18N_LANGUAGES, TRAINING_TYPE } from "utils/consts"; import { HubsDropdown } from "../AdminDropdowns"; @@ -26,6 +27,7 @@ import { import { DeleteOutlined, EditOutlined, + HolderOutlined, PlusCircleOutlined, TeamOutlined, } from "@ant-design/icons"; @@ -35,6 +37,69 @@ import "components/css/Training.scss"; import AdminDownloadDropdown from "../AdminDownloadDropdown"; import TrainingTranslationModal from "../TrainingTranslationModal"; import UpdateTrainingForm from "../UpdateTrainingModal"; +import { + arrayMove, + SortableContext, + useSortable, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { DndContext } from "@dnd-kit/core"; +import { CSS } from "@dnd-kit/utilities"; +import { restrictToVerticalAxis } from "@dnd-kit/modifiers"; + +const RowContext = React.createContext({}); +const DragHandle = () => { + const { setActivatorNodeRef, listeners } = useContext(RowContext); + return ( +