diff --git a/backend/api/__init__.py b/backend/api/__init__.py index 1dd4d168..2997a666 100644 --- a/backend/api/__init__.py +++ b/backend/api/__init__.py @@ -99,6 +99,7 @@ def create_app(): masters, translation, events, + announcement, meeting, ) @@ -124,6 +125,7 @@ def create_app(): app.register_blueprint(translation.translation, url_prefix="/api/translation") app.register_blueprint(events.event, url_prefix="/api") + app.register_blueprint(announcement.announcement, url_prefix="/api") app.register_error_handler(Exception, all_exception_handler) diff --git a/backend/api/models/Announcement.py b/backend/api/models/Announcement.py new file mode 100644 index 00000000..76ce8cc6 --- /dev/null +++ b/backend/api/models/Announcement.py @@ -0,0 +1,29 @@ +from tokenize import String +from api.core import Mixin +from .base import db +from mongoengine import * +from api.models import Translations, Image + + +class Announcement(Document, Mixin): + """Model for Announcement.""" + + name = StringField(required=True) + description = StringField(required=True) + nameTranslated = DictField(required=False) + descriptionTranslated = DictField(required=False) + date_submitted = DateTimeField(required=True) + role = StringField(required=True) + filee = FileField() + translations = EmbeddedDocumentField(Translations, required=False) + file_name = StringField() + image = EmbeddedDocumentField(Image, required=False) + hub_id = StringField(required=False) + hub_user = DictField(required=False) + partner_id = StringField(required=False) + mentor_id = StringField(required=False) + mentee_id = StringField(required=False) + + def __repr__(self): + return f"""""" diff --git a/backend/api/models/Training.py b/backend/api/models/Training.py index 658c70c0..178718b7 100644 --- a/backend/api/models/Training.py +++ b/backend/api/models/Training.py @@ -24,6 +24,9 @@ class Training(Document, Mixin): hub_id = StringField(required=False) hub_user = DictField(required=False) signed_data = DictField(required=False) + partner_id = StringField(required=False) + mentor_id = StringField(required=False) + mentee_id = StringField(required=False) def __repr__(self): return f"""", methods=["GET"]) +def get_announcements(role): + lang = request.args.get("lang", "en-US") + hub_user_id = request.args.get("hub_user_id", None) + user_id = request.args.get("user_id", None) + + if int(role) == Account.HUB: + data = Announcement.objects.filter(role__in=[role], hub_id=str(hub_user_id)) + else: + data = Announcement.objects(role__in=[role]) + + append_data = [] + if int(role) == Account.MENTOR: + if user_id is None: + append_data = Announcement.objects( + Q(mentor_id__ne=None) & Q(mentor_id__ne="") + ) + else: + append_data = Announcement.objects(mentor_id=user_id) + if int(role) == Account.MENTEE: + if user_id is None: + append_data = Announcement.objects( + Q(mentee_id__ne=None) & Q(mentee_id__ne="") + ) + else: + append_data = Announcement.objects(mentee_id=user_id) + + result = [] + + if lang in TARGET_LANGS: + for item in data: + item_dic = json.loads(item.to_json()) + item_dic["name"] = item.nameTranslated.get(lang, item.name) + item_dic["description"] = item.descriptionTranslated.get( + lang, item.description + ) + result.append(item_dic) + for item in append_data: + item_dic = json.loads(item.to_json()) + item_dic["name"] = item.nameTranslated.get(lang, item.name) + item_dic["description"] = item.descriptionTranslated.get( + lang, item.description + ) + result.append(item_dic) + else: + for item in data: + result.append(item) + for item in append_data: + result.append(item) + + return create_response(data={"res": result}) + + +@announcement.route("announcement/register/", methods=["POST"]) +def new_announce(role): + try: + name = request.form["name"] + nameTranslated = get_all_translations(request.form["description"]) + description = request.form["description"] + descriptionTranslated = get_all_translations(request.form["description"]) + hub_id = None + + if "hub_id" in request.form: + hub_id = request.form["hub_id"] + + partner_id = None + if ( + "partner_id" in request.form + and request.form["partner_id"] is not None + and request.form["partner_id"] != "" + ): + partner_id = request.form["partner_id"] + mentor_id = None + if ( + "mentor_id" in request.form + and request.form["mentor_id"] is not None + and request.form["mentor_id"] != "" + ): + mentor_id = request.form["mentor_id"] + mentee_id = None + if ( + "mentee_id" in request.form + and request.form["mentee_id"] is not None + and request.form["mentee_id"] != "" + ): + mentee_id = request.form["mentee_id"] + + announce = Announcement( + name=name, + nameTranslated=nameTranslated, + description=description, + descriptionTranslated=descriptionTranslated, + role=str(role), + date_submitted=datetime.now(), + hub_id=hub_id, + partner_id=partner_id, + mentor_id=mentor_id, + mentee_id=mentee_id, + ) + + document = request.files.get("document", None) + if document: + file_name = secure_filename(document.filename) + if file_name == "": + return create_response(status=400, message="Missing file name") + announce.file_name = file_name + announce.filee.put(document, filename=file_name) + + announce.save() + + front_url = request.form["front_url"] + send_notification = ( + True if request.form["send_notification"] == "true" else False + ) + if send_notification == True: + new_id = announce.id + hub_url = "" + if int(role) == Account.MENTOR: + recipients = MentorProfile.objects.only("email", "preferred_language") + elif int(role) == Account.MENTEE: + recipients = MenteeProfile.objects.only("email", "preferred_language") + elif int(role) == Account.PARTNER: + if partner_id is not None: + recipients = [] + partner_data = PartnerProfile.objects.filter(id=partner_id).only( + "email", "preferred_language" + ) + for item in partner_data: + recipients.append(item) + if mentor_id is not None: + mentor_data = MentorProfile.objects.filter(id=mentor_id).only( + "email", "preferred_language" + ) + for item in mentor_data: + item.mentor = "mentor" + recipients.append(item) + if mentee_id is not None: + mentee_data = MenteeProfile.objects.filter(id=mentee_id).only( + "email", "preferred_language" + ) + for item in mentee_data: + item.mentee = "mentee" + recipients.append(item) + else: + recipients = PartnerProfile.objects.only( + "email", "preferred_language" + ) + else: + hub_users = Hub.objects.filter(id=hub_id).only( + "email", "preferred_language", "url" + ) + partners = PartnerProfile.objects.filter(hub_id=hub_id).only( + "email", "preferred_language" + ) + recipients = [] + for hub_user in hub_users: + recipients.append(hub_user) + if hub_url == "": + hub_url = hub_user.url + "/" + for partner_user in partners: + recipients.append(partner_user) + target_url = front_url + hub_url + "announcement/" + str(new_id) + + for recipient in recipients: + res, res_msg = send_email( + recipient=recipient.email, + data={ + "link": target_url, + "title": announce.name, + "announcement": announce.description, + "subject": "New Announcement", + }, + template_id=NEW_ANNOUNCE_TEMPLATE, + ) + if not res: + msg = "Failed to send new traing data alert email " + res_msg + logger.error(msg) + + except Exception as e: + return create_response(status=400, message=f"missing parameters {e}") + + return create_response(status=200, data={"announce": announce}) + + +@announcement.route("announcement/edit/", methods=["PUT"]) +def edit(id): + try: + announcement = Announcement.objects.get(id=id) + except Exception as e: + return create_response(status=422, message=f"Failed to get training: {e}") + + new_name = request.form.get("name", announcement.name) + new_description = request.form.get("description", announcement.description) + + hub_id = None + if "hub_id" in request.form: + hub_id = request.form["hub_id"] + announcement.hub_id = hub_id + + if announcement.name != new_name: + announcement.name = new_name + announcement.nameTranslated = get_all_translations(new_name) + if announcement.description != new_description: + announcement.description = new_description + announcement.descriptionTranslated = get_all_translations(new_description) + + announcement.role = str(request.form.get("role", announcement.role)) + + announcement.partner_id = request.form.get("partner_id", announcement.partner_id) + announcement.mentor_id = request.form.get("mentor_id", announcement.mentor_id) + announcement.mentee_id = request.form.get("mentee_id", announcement.mentee_id) + + document = request.files.get("document", None) + if document: + file_name = secure_filename(document.filename) + if file_name == "": + return create_response(status=400, message="Missing file name") + + announcement.filee.replace(document, filename=file_name) + announcement.file_name = file_name + + announcement.save() + + return create_response(status=200, data={"announce": announcement}) + + +@announcement.route("announcement/upload//image", methods=["PUT"]) +def uploadImage(id): + announcement = Announcement.objects.get(id=id) + if announcement: + try: + if ( + announcement.image is True + and announcement.announcement.image_hash is True + ): + image_response = imgur_client.delete_image( + announcement.image_file.image_hash + ) + + image = request.files["image"] + image_response = imgur_client.send_image(image) + new_image = Image( + url=image_response["data"]["link"], + image_hash=image_response["data"]["deletehash"], + ) + announcement.image = new_image + announcement.save() + return create_response(status=200, message=f"Image upload success") + except: + return create_response(status=400, message=f"Image upload failed") + else: + return create_response(status=400, message=f"Image upload failed") + + +@announcement.route("/announcement/getDoc/", methods=["GET"]) +def get_doc_file(id): + lang = request.args.get("lang", "en-US") + try: + announcement = Announcement.objects.get(id=id) + except: + return create_response(status=422, message="training not found") + + if lang in TARGET_LANGS: + document = get_translation_document(announcement.translations, lang) + else: + document = announcement.filee.read() + + content_type = announcement.filee.content_type + + if not document: + return create_response(status=422, message="No document found") + return send_file( + BytesIO(document), download_name=announcement.file_name, mimetype=content_type + ) + + +@announcement.route("announcement/get/", methods=["GET"]) +def get_announce_by_id(id): + try: + announcement = Announcement.objects.get(id=id) + except: + return create_response(status=422, message="announcement not found") + + return create_response(data={"announcement": announcement}) + + +@announcement.route("announcement/delete/", methods=["DELETE"]) +def delete(id): + try: + announcement = Announcement.objects.get(id=id) + announcement.delete() + except: + return create_response(status=422, message="event not found") + + return create_response(status=200, message="Successful deletion") diff --git a/backend/api/views/training.py b/backend/api/views/training.py index 0de732a0..ef8236a8 100644 --- a/backend/api/views/training.py +++ b/backend/api/views/training.py @@ -34,6 +34,7 @@ ) from api.utils.request_utils import send_email import pytz +from mongoengine.queryset.visitor import Q training = Blueprint("training", __name__) # initialize blueprint @@ -77,7 +78,21 @@ def getSignedData(role): def get_trainings(role): lang = request.args.get("lang", "en-US") user_email = request.args.get("user_email", None) + user_id = request.args.get("user_id", None) trainings = Training.objects(role=str(role)) + + append_data = [] + if int(role) == Account.MENTOR: + if user_id is None: + append_data = Training.objects(Q(mentor_id__ne=None) & Q(mentor_id__ne="")) + else: + append_data = Training.objects(mentor_id=user_id) + if int(role) == Account.MENTEE: + if user_id is None: + append_data = Training.objects(Q(mentee_id__ne=None) & Q(mentee_id__ne="")) + else: + append_data = Training.objects(mentee_id=user_id) + result = [] signed_trainings = {} if user_email is not None: @@ -105,6 +120,15 @@ def get_trainings(role): } temp.append(training) + for training in append_data: + if training.hub_id is not None: + training.hub_user = Hub_users_object[str(training.hub_id)] + if str(training.id) in signed_trainings: + training.signed_data = { + str(training.id): signed_trainings[str(training.id)] + } + temp.append(training) + trainings = temp Hub_users = Hub.objects() @@ -225,7 +249,15 @@ def get_train_id_edit(id): train.role = str(request.form.get("role", train.role)) train.typee = request.form.get("typee", train.typee) train.isVideo = isVideo - train.requried_sign = True if request.form["requried_sign"] == "true" else False + requried_sign = False + if "requried_sign" in request.form and request.form["requried_sign"] == "true": + requried_sign = True + train.requried_sign = requried_sign + + train.partner_id = request.form.get("partner_id", train.partner_id) + train.mentor_id = request.form.get("mentor_id", train.mentor_id) + train.mentee_id = request.form.get("mentee_id", train.mentee_id) + if not isVideo and request.form.get("isNewDocument", False) == "true": document = request.files.get("document", None) if not document: @@ -332,11 +364,35 @@ def new_train(role): descriptionTranslated = get_all_translations(request.form["description"]) typee = request.form["typee"] isVideo = True if request.form["isVideo"] == "true" else False - requried_sign = True if request.form["requried_sign"] == "true" else False + requried_sign = False + if "requried_sign" in request.form and request.form["requried_sign"] == "true": + requried_sign = True + hub_id = None if "hub_id" in request.form: hub_id = request.form["hub_id"] + partner_id = None + if ( + "partner_id" in request.form + and request.form["partner_id"] is not None + and request.form["partner_id"] != "" + ): + partner_id = request.form["partner_id"] + mentor_id = None + if ( + "mentor_id" in request.form + and request.form["mentor_id"] is not None + and request.form["mentor_id"] != "" + ): + mentor_id = request.form["mentor_id"] + mentee_id = None + if ( + "mentee_id" in request.form + and request.form["mentee_id"] is not None + and request.form["mentee_id"] != "" + ): + mentee_id = request.form["mentee_id"] train = Training( name=name, @@ -349,6 +405,9 @@ def new_train(role): requried_sign=requried_sign, date_submitted=datetime.now(), hub_id=hub_id, + partner_id=partner_id, + mentor_id=mentor_id, + mentee_id=mentee_id, ) if not isVideo: document = request.files.get("document", None) @@ -375,7 +434,31 @@ def new_train(role): elif int(role) == Account.MENTEE: recipients = MenteeProfile.objects.only("email", "preferred_language") elif int(role) == Account.PARTNER: - recipients = PartnerProfile.objects.only("email", "preferred_language") + if partner_id is not None: + recipients = [] + partner_data = PartnerProfile.objects.filter(id=partner_id).only( + "email", "preferred_language" + ) + for item in partner_data: + recipients.append(item) + if mentor_id is not None: + mentor_data = MentorProfile.objects.filter(id=mentor_id).only( + "email", "preferred_language" + ) + for item in mentor_data: + item.mentor = "mentor" + recipients.append(item) + if mentee_id is not None: + mentee_data = MenteeProfile.objects.filter(id=mentee_id).only( + "email", "preferred_language" + ) + for item in mentee_data: + item.mentee = "mentee" + recipients.append(item) + else: + recipients = PartnerProfile.objects.only( + "email", "preferred_language" + ) else: hub_users = Hub.objects.filter(id=hub_id).only( "email", "preferred_language", "url" @@ -396,6 +479,24 @@ def new_train(role): ) for recipient in recipients: + if recipient.mentor == "mentor": + target_url = ( + front_url + + hub_url + + "new_training/" + + str(Account.MENTOR) + + "/" + + str(new_train_id) + ) + if recipient.mentee == "mentee": + target_url = ( + front_url + + hub_url + + "new_training/" + + str(Account.MENTEE) + + "/" + + str(new_train_id) + ) res, res_msg = send_email( recipient=recipient.email, data={ diff --git a/frontend/public/locales/ar/translation.json b/frontend/public/locales/ar/translation.json index b67953cf..dfadcd9b 100644 --- a/frontend/public/locales/ar/translation.json +++ b/frontend/public/locales/ar/translation.json @@ -581,6 +581,7 @@ "profile": "حساب تعريفي", "videos": "مقاطع الفيديو الخاصة بك", "events":"الأحداث", + "announcements":"الإعلانات", "training": "تدريبات", "invite_link":"رابط الدعوة", "generate_invite_link":"إنشاء رابط الدعوة", diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index d6b33ccf..4c5e4080 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -582,6 +582,7 @@ "profile": "Profile", "videos": "Your Videos", "events":"Events", + "announcements":"Announcements", "training":"Training", "invite_link":"Invite Link", "generate_invite_link":"Generate Invite Link", diff --git a/frontend/public/locales/es-US/translation.json b/frontend/public/locales/es-US/translation.json index d6c9111d..defbd821 100644 --- a/frontend/public/locales/es-US/translation.json +++ b/frontend/public/locales/es-US/translation.json @@ -581,6 +581,7 @@ "profile": "Perfil", "videos": "Tus vídeos", "events":"Eventos", + "announcements":"Anuncios", "training": "Entrenamiento", "invite_link":"Enlace de invitación", "generate_invite_link":"Generar enlace de invitación", diff --git a/frontend/public/locales/fa-AF/translation.json b/frontend/public/locales/fa-AF/translation.json index f15cdb5d..d10da679 100644 --- a/frontend/public/locales/fa-AF/translation.json +++ b/frontend/public/locales/fa-AF/translation.json @@ -581,6 +581,7 @@ "profile": "مشخصات", "videos":"ویدیوهای شما", "events":"مناسبت ها", + "announcements":"اطلاعیه ها", "training": "آموزش", "invite_link":"لینک دعوت", "generate_invite_link":"ایجاد پیوند دعوت", diff --git a/frontend/public/locales/pa-AR/translation.json b/frontend/public/locales/pa-AR/translation.json index 2e8b3683..bcddd4e0 100644 --- a/frontend/public/locales/pa-AR/translation.json +++ b/frontend/public/locales/pa-AR/translation.json @@ -579,6 +579,7 @@ "sidebars": { "appointments": "ملاقاتونه", "events": "پيښې", + "announcements":"اعلانونه", "explore": "سپړنه", "profile": "پروفایل ", "videos": "ستاسو ویدیوګانې", diff --git a/frontend/public/locales/pt-BR/translation.json b/frontend/public/locales/pt-BR/translation.json index f2871d59..e349ba61 100644 --- a/frontend/public/locales/pt-BR/translation.json +++ b/frontend/public/locales/pt-BR/translation.json @@ -581,6 +581,7 @@ "profile": "Perfil", "videos": "Seus vídeos", "events":"Eventos", + "announcements":"Anúncios", "training": "Treinamento", "invite_link":"Link de convite", "generate_invite_link":"Gerar link de convite", diff --git a/frontend/src/app/App.js b/frontend/src/app/App.js index 981e87bf..12c35013 100644 --- a/frontend/src/app/App.js +++ b/frontend/src/app/App.js @@ -23,6 +23,7 @@ import GroupMessages from "components/pages/GroupMessages"; import ApplicationForm from "components/pages/ApplicationForm"; import SocketComponent from "components/SocketComponent"; import AdminTraining from "components/pages/AdminTraining"; +import AdminAnnouncement from "components/pages/AdminAnnouncement"; import AdminSign from "components/pages/AdminSign"; import TrainingData from "components/pages/TrainingData"; import EventDetail from "components/pages/EventDetail"; @@ -45,9 +46,11 @@ import PublicRoute from "components/PublicRoute"; import Training from "components/pages/Training"; import BuildProfile from "components/pages/BuildProfile"; import Events from "components/pages/Events"; +import Announcements from "components/pages/Announcements"; +import AnnouncementDetail from "components/pages/AnnouncementDetail"; import HubInviteLink from "components/pages/HubInviteLink"; import { useSelector } from "react-redux"; -import { ACCOUNT_TYPE, FRONT_BASE_URL } from "utils/consts"; +import { ACCOUNT_TYPE } from "utils/consts"; import { fetchAccounts } from "utils/api"; import CreateMeetingLink from "components/CreateMeetingLink"; import MeetingPanel from "components/MeetingPanel"; @@ -544,6 +547,21 @@ function App() { )} + + {role == ACCOUNT_TYPE.ADMIN ? ( + + ) : ( + <> + {cur_time - startPathTime > 100 && ( + + )} + + )} + {role == ACCOUNT_TYPE.ADMIN ? ( @@ -648,6 +666,12 @@ function App() { + + + + + + diff --git a/frontend/src/components/TrainingList.js b/frontend/src/components/TrainingList.js index 23fd6aa8..c65f5a3c 100644 --- a/frontend/src/components/TrainingList.js +++ b/frontend/src/components/TrainingList.js @@ -188,7 +188,11 @@ const TrainingList = (props) => { useEffect(() => { setLoading(true); - getTrainings(props.role, user ? user.email : props.user_email) + getTrainings( + props.role, + user ? user.email : props.user_email, + user ? user._id.$oid : null + ) .then((trains) => { if (props.role == ACCOUNT_TYPE.HUB && user) { var hub_user_id = null; diff --git a/frontend/src/components/UpdateAnnouncementModal.js b/frontend/src/components/UpdateAnnouncementModal.js new file mode 100644 index 00000000..6c75c795 --- /dev/null +++ b/frontend/src/components/UpdateAnnouncementModal.js @@ -0,0 +1,304 @@ +import React, { useEffect, useState } from "react"; +import { Button, Form, Input, Modal, Select, Upload, Checkbox } from "antd"; +import { UploadOutlined } from "@ant-design/icons"; +import { ACCOUNT_TYPE } from "utils/consts"; +import "components/css/Training.scss"; +import ImgCrop from "antd-img-crop"; + +const { Option } = Select; + +const normFile = (e) => { + if (Array.isArray(e)) { + return e; + } + return e?.fileList; +}; + +// TODO: Change the weird names of some of the forms like typee and filee +function UpdateAnnouncementModal({ + onCancel, + onFinish, + open, + currentAnnounce, + loading, + hubOptions, + partnerOptions, +}) { + const [form] = Form.useForm(); + const [valuesChanged, setValuesChanged] = useState(false); + const [role, setRole] = useState(null); + const [mentors, setMentors] = useState([]); + const [mentees, setMentees] = useState([]); + const [image, setImage] = useState( + currentAnnounce && currentAnnounce.image ? currentAnnounce.image : null + ); + const [changedImage, setChangedImage] = useState(false); + const newAnnouncement = !currentAnnounce; + + const handleValuesChange = (changedValues, allValues) => { + setValuesChanged(true); + }; + + const onOk = () => { + if (valuesChanged) { + form + .validateFields() + .then((values) => { + values.document = values.document?.[0]?.originFileObj; + onFinish( + { + ...values, + }, + newAnnouncement, + image, + changedImage + ); + }) + .catch((info) => { + console.error("Validate Failed:", info); + }); + } else { + onCancel(); + } + }; + + useEffect(() => { + form.resetFields(); + setValuesChanged(false); + if (currentAnnounce) { + currentAnnounce.role = parseInt(currentAnnounce.role); + form.setFieldsValue(currentAnnounce); + form.setFieldValue("document", [ + { + name: currentAnnounce.file_name, + }, + ]); + setImage( + currentAnnounce && currentAnnounce.image ? currentAnnounce.image : null + ); + } else { + setImage(null); + form.setFieldValue("send_notification", true); + } + }, [open, currentAnnounce]); + + const setMentorMentees = (partner_id) => { + var partner_data = partnerOptions.find((x) => x.value === partner_id); + if (partner_data) { + setMentees(partner_data.assign_mentees); + setMentors(partner_data.assign_mentors); + } + }; + + const changeRole = (val) => { + setRole(val); + if (val !== ACCOUNT_TYPE.PARTNER) { + form.setFieldValue("partner_id", ""); + form.setFieldValue("mentor_id", ""); + form.setFieldValue("mentee_id", ""); + setMentees([]); + setMentors([]); + } + }; + + return ( + +
+ + + + {newAnnouncement && ( + + Send Notification + + )} + + false}> + + + + + + + + { + setImage(file.file.originFileObj); + setChangedImage(true); + }} + accept=".png,.jpg,.jpeg" + showUploadList={false} + > + + + + + {image && ( + + )} + + + + {form.getFieldValue("role") === ACCOUNT_TYPE.PARTNER && ( + + + + )} + {mentors && mentors.length > 0 && ( + + + + )} + {mentees && mentees.length > 0 && ( + + + + )} + {role === ACCOUNT_TYPE.HUB && ( + + + + )} +
+
+ ); +} + +export default UpdateAnnouncementModal; diff --git a/frontend/src/components/UpdateTrainingModal.js b/frontend/src/components/UpdateTrainingModal.js index 66094fcd..c96517d3 100644 --- a/frontend/src/components/UpdateTrainingModal.js +++ b/frontend/src/components/UpdateTrainingModal.js @@ -30,12 +30,15 @@ function UpdateTrainingModal({ currentTraining, loading, hubOptions, + partnerOptions, }) { const [form] = Form.useForm(); const [trainingType, setTrainingType] = useState(""); const [isNewDocument, setIsNewDocument] = useState(false); const [valuesChanged, setValuesChanged] = useState(false); const [role, setRole] = useState(null); + const [mentors, setMentors] = useState([]); + const [mentees, setMentees] = useState([]); const newTraining = !currentTraining; const handleValuesChange = (changedValues, allValues) => { @@ -90,6 +93,25 @@ function UpdateTrainingModal({ } }, [open, currentTraining]); + const setMentorMentees = (partner_id) => { + var partner_data = partnerOptions.find((x) => x.value === partner_id); + if (partner_data) { + setMentees(partner_data.assign_mentees); + setMentors(partner_data.assign_mentors); + } + }; + + const changeRole = (val) => { + setRole(val); + if (val !== ACCOUNT_TYPE.PARTNER) { + form.setFieldValue("partner_id", ""); + form.setFieldValue("mentor_id", ""); + form.setFieldValue("mentee_id", ""); + setMentees([]); + setMentors([]); + } + }; + return ( - { + changeRole(val); + }} + > + {form.getFieldValue("role") === ACCOUNT_TYPE.PARTNER && ( + + + + )} + {mentors && mentors.length > 0 && ( + + + + )} + {mentees && mentees.length > 0 && ( + + + + )} {role === ACCOUNT_TYPE.HUB && ( { + const [role, setRole] = useState(ACCOUNT_TYPE.MENTEE); + const [announceData, setAnnounceData] = useState([]); + const [reload, setReload] = useState(true); + // const [documentCost, setDocumentCost] = useState(null); + const [announceId, setAnnounceId] = useState(null); + const [openUpdateAnnounce, setOpenUpdateAnnounce] = useState(false); + const [currentAnnounce, setCurrentAnnounce] = useState(null); + const [loading, setLoading] = useState(false); + const [hubOptions, setHubOptions] = useState([]); + const [resetFilters, setResetFilters] = useState(false); + const [partnerOptions, setPartnerOptions] = useState([]); + const [allData, setAllData] = useState([]); + + const onCancelAnnounceForm = () => { + setAnnounceId(null); + setCurrentAnnounce(null); + setOpenUpdateAnnounce(false); + }; + + useEffect(() => { + async function getHubData() { + var temp = []; + const hub_data = await fetchAccounts(ACCOUNT_TYPE.HUB); + hub_data.map((hub_item) => { + temp.push({ label: hub_item.name, value: hub_item._id.$oid }); + return true; + }); + setHubOptions(temp); + } + async function getPartners() { + var temp = []; + const data = await fetchPartners(); + data.map((item) => { + temp.push({ + label: item.organization, + value: item._id.$oid, + assign_mentees: item.assign_mentees, + assign_mentors: item.assign_mentors, + }); + return true; + }); + setPartnerOptions(temp); + } + getHubData(); + getPartners(); + }, []); + + const handleResetFilters = () => { + setResetFilters(!resetFilters); + setAnnounceData(allData); + }; + + const onFinishAnnounceForm = async ( + values, + isNewAnnounce, + image, + changedImage + ) => { + setLoading(true); + let res = null; + if (isNewAnnounce) { + message.loading("Announcing new Announcement...", 3); + res = await newAnnounceCreate(values); + if (!res?.success) { + notification.error({ + message: "ERROR", + description: `Couldn't create new Announcement`, + }); + } else { + notification.success({ + message: "SUCCESS", + description: "New Announcement has been created successfully", + }); + } + } else { + res = await EditAnnounceById(announceId, values); + if (!res?.success) { + notification.error({ + message: "ERROR", + description: `Couldn't update Announcement`, + }); + } else { + notification.success({ + message: "SUCCESS", + description: "Announcement has been updated successfully", + }); + } + } + if (image && changedImage) { + if (res && res.success) { + await uploadAnnounceImage(image, res.result.announce._id.$oid); + } + } + setLoading(false); + setAnnounceId(null); + setReload(!reload); + setCurrentAnnounce(null); + setOpenUpdateAnnounce(false); + }; + + const openUpdateAnnounceModal = (id = null) => { + if (id) { + setAnnounceId(id); + getAnnounceById(id).then((res) => { + if (res) { + setCurrentAnnounce(res); + setOpenUpdateAnnounce(true); + } + }); + } else { + setAnnounceId(null); + setCurrentAnnounce(null); + setOpenUpdateAnnounce(true); + } + }; + + const handleAnnounceDownload = async (record, lang) => { + let response = await getAnnounceDoc(record.id, lang); + if (!response) { + notification.error({ + message: "ERROR", + description: "Couldn't download file", + }); + return; + } + downloadBlob(response, record.file_name); + }; + + const deleteAnnounce = async (id) => { + const success = await deleteAnnouncebyId(id); + if (success) { + notification.success({ + message: "SUCCESS", + description: "Announcement has been deleted successfully", + }); + setReload(!reload); + } else { + notification.error({ + message: "ERROR", + description: `Couldn't delete Announcement`, + }); + } + }; + + const getAvailableLangs = (record) => { + if (!record?.translations) return [I18N_LANGUAGES[0]]; + let items = I18N_LANGUAGES.filter((lang) => { + return ( + Object.keys(record?.translations).includes(lang.value) && + record?.translations[lang.value] !== null + ); + }); + // Appends English by default + items.unshift(I18N_LANGUAGES[0]); + return items; + }; + + useMemo(() => { + const getData = async () => { + setLoading(true); + let newData = await getAnnouncements(role); + if (newData) { + setAnnounceData(newData); + setAllData(newData); + } else { + setAnnounceData([]); + setAllData([]); + notification.error({ + message: "ERROR", + description: "Couldn't get Announcements", + }); + } + setLoading(false); + }; + getData(); + }, [role, reload]); + + const columns = [ + { + title: "Name", + dataIndex: "name", + key: "name", + render: (name) => <>{name}, + }, + { + title: "Description", + dataIndex: "description", + key: "description", + render: (description) => <>{description}, + }, + { + title: "Document", + dataIndex: "file_name", + key: "file_name", + render: (file_name, record) => { + if (file_name && file_name !== "") { + return ( + handleAnnounceDownload(record, lang)} + /> + ); + } + }, + }, + { + title: "Image", + dataIndex: "image", + key: "image", + render: (image) => { + if (image) { + return ; + } + }, + }, + { + title: "Hub User", + dataIndex: "hub_user", + key: "hub_user", + render: (hub_user) => <>{hub_user.name}, + align: "center", + }, + { + title: "Edit", + dataIndex: "id", + key: "id", + render: (id) => ( + openUpdateAnnounceModal(id)} + /> + ), + + align: "center", + }, + { + title: "Delete", + dataIndex: "id", + key: "id", + render: (id) => ( + { + deleteAnnounce(id); + }} + onCancel={() => message.info(`No deletion has been made`)} + okText="Yes" + cancelText="No" + > + + + ), + align: "center", + }, + ]; + + const searchbyHub = (hub_id) => { + if (role === ACCOUNT_TYPE.HUB) { + setAnnounceData(allData.filter((x) => x.hub_id == hub_id)); + } + }; + + const tabItems = [ + { + label: `Mentee`, + key: ACCOUNT_TYPE.MENTEE, + disabled: false, + }, + { + label: `Mentor`, + key: ACCOUNT_TYPE.MENTOR, + disabled: false, + }, + { + label: `Partner`, + key: ACCOUNT_TYPE.PARTNER, + disabled: false, + }, + { + label: `Hub`, + key: ACCOUNT_TYPE.HUB, + disabled: false, + }, + ]; + + return ( +
+ { + setRole(key); + setResetFilters(!resetFilters); + }} + items={tabItems} + /> +
+ +
Hub
+ searchbyHub(key)} + onReset={resetFilters} + /> + +
+ + + + + + + + ); +}; + +export default withRouter(AdminAnnouncement); diff --git a/frontend/src/components/pages/AdminTraining.js b/frontend/src/components/pages/AdminTraining.js index 60939f75..75c86ade 100644 --- a/frontend/src/components/pages/AdminTraining.js +++ b/frontend/src/components/pages/AdminTraining.js @@ -7,6 +7,7 @@ import { getTrainings, getTrainVideo, fetchAccounts, + fetchPartners, newTrainCreate, } from "utils/api"; import { ACCOUNT_TYPE, I18N_LANGUAGES, TRAINING_TYPE } from "utils/consts"; @@ -48,6 +49,7 @@ const AdminTraining = () => { const [loading, setLoading] = useState(false); const [hubOptions, setHubOptions] = useState([]); const [resetFilters, setResetFilters] = useState(false); + const [partnerOptions, setPartnerOptions] = useState([]); const [allData, setAllData] = useState([]); const onCancelTrainingForm = () => { @@ -66,7 +68,22 @@ const AdminTraining = () => { }); setHubOptions(temp); } + async function getPartners() { + var temp = []; + const data = await fetchPartners(); + data.map((item) => { + temp.push({ + label: item.organization, + value: item._id.$oid, + assign_mentees: item.assign_mentees, + assign_mentors: item.assign_mentors, + }); + return true; + }); + setPartnerOptions(temp); + } getHubData(); + getPartners(); }, []); const handleResetFilters = () => { @@ -399,6 +416,7 @@ const AdminTraining = () => { currentTraining={currentTraining} loading={loading} hubOptions={hubOptions} + partnerOptions={partnerOptions} /> ); diff --git a/frontend/src/components/pages/AnnouncementDetail.js b/frontend/src/components/pages/AnnouncementDetail.js new file mode 100644 index 00000000..6f8fe8b6 --- /dev/null +++ b/frontend/src/components/pages/AnnouncementDetail.js @@ -0,0 +1,170 @@ +import React, { useEffect, useState } from "react"; +import { Typography, notification } from "antd"; +import { withRouter } from "react-router-dom"; +import { useAuth } from "../../utils/hooks/useAuth"; +import { getAnnounceDoc, getAnnounceById, downloadBlob } from "utils/api"; +import { useTranslation } from "react-i18next"; +import { I18N_LANGUAGES } from "utils/consts"; +import { useSelector } from "react-redux"; +import AdminDownloadDropdown from "../AdminDownloadDropdown"; + +const { Title, Paragraph } = Typography; + +function AnnouncementDetail({ match }) { + const id = match.params.id; + const [announce, setAnnounce] = useState({}); + const { isHub } = useAuth(); + const { t } = useTranslation(); + const { user } = useSelector((state) => state.user); + + useEffect(() => { + async function getData(hub_user_id = null) { + const data = await getAnnounceById(id); + if (data) { + setAnnounce(data); + } + } + var hub_user_id = null; + if (isHub && user) { + if (user.hub_id) { + hub_user_id = user.hub_id; + } else { + hub_user_id = user._id.$oid; + } + } + getData(hub_user_id); + }, [id]); + + const getAvailableLangs = (record) => { + if (!record?.translations) return [I18N_LANGUAGES[0]]; + let items = I18N_LANGUAGES.filter((lang) => { + return ( + Object.keys(record?.translations).includes(lang.value) && + record?.translations[lang.value] !== null + ); + }); + // Appends English by default + items.unshift(I18N_LANGUAGES[0]); + return items; + }; + + const handleAnnounceDownload = async (record, lang) => { + let response = await getAnnounceDoc(record._id.$oid, lang); + if (!response) { + notification.error({ + message: "ERROR", + description: "Couldn't download file", + }); + return; + } + downloadBlob(response, record.file_name); + }; + + return ( + <> +
+
+
+
+
+ + {announce && announce.name} + +
+
+ +
+ {announce.image && ( + + + {t("events.attatchment")}: + + + + )} + {announce.description && ( + + + {t("events.summary")}: + + + {announce.description} + + + )} +
+ {announce.file_name && ( + <> + + + {"File"}: + + + + handleAnnounceDownload(announce, lang) + } + /> + + + + )} +
+
+
+
+
+ + ); +} + +export default withRouter(AnnouncementDetail); diff --git a/frontend/src/components/pages/Announcements.js b/frontend/src/components/pages/Announcements.js new file mode 100644 index 00000000..cdac4d53 --- /dev/null +++ b/frontend/src/components/pages/Announcements.js @@ -0,0 +1,266 @@ +import React, { useState, useEffect } from "react"; +import { getAnnouncements, getAnnounceDoc, downloadBlob } from "utils/api"; +import { + Input, + Modal, + Spin, + theme, + Typography, + Affix, + Button, + FloatButton, + Tooltip, + notification, + Table, +} from "antd"; +import { NavLink } from "react-router-dom"; +import { SearchOutlined } from "@ant-design/icons"; +import "../css/Gallery.scss"; +import { useTranslation } from "react-i18next"; +import { css } from "@emotion/css"; +import { ACCOUNT_TYPE, I18N_LANGUAGES } from "utils/consts"; +import { useSelector } from "react-redux"; +import { getRole } from "utils/auth.service"; +import AdminDownloadDropdown from "../AdminDownloadDropdown"; + +const { Title } = Typography; + +function Announcements() { + const { + token: { colorPrimaryBg }, + } = theme.useToken(); + const { t } = useTranslation(); + const [allData, setAllData] = useState([]); + const [query, setQuery] = useState(); + const [mobileFilterVisible, setMobileFilterVisible] = useState(false); + const [pageLoaded, setPageLoaded] = useState(false); + const [hubUrl, setHubUrl] = useState(""); + const { user } = useSelector((state) => state.user); + const role = getRole(); + useEffect(() => { + async function getData(hub_user_id) { + const all_data = await getAnnouncements( + role, + user?._id.$oid, + hub_user_id + ); + setAllData(all_data); + setPageLoaded(true); + } + var hub_user_id = null; + if (role == ACCOUNT_TYPE.HUB && user) { + if (user.hub_id) { + hub_user_id = user.hub_id; + if (user.hub_user) { + setHubUrl("/" + user.hub_user.url); + } + } else { + hub_user_id = user._id.$oid; + setHubUrl("/" + user.url); + } + } + getData(hub_user_id); + }, []); + + const getFilteredData = () => { + return allData.filter((item) => { + // matches is true if no options selected, or if mentor has AT LEAST one of the selected options + const matchesText = + !query || + item.name.toUpperCase().includes(query.toUpperCase()) || + (item.description && + item.description.toUpperCase().includes(query.toUpperCase())) || + (item.name && item.name.toUpperCase().includes(query.toUpperCase())); + + return matchesText; + }); + }; + + const getFilterForm = () => ( + <> + + {t("gallery.filterBy")} + + } + value={query} + onChange={(e) => setQuery(e.target.value)} + /> + + ); + + const getAvailableLangs = (record) => { + if (!record?.translations) return [I18N_LANGUAGES[0]]; + let items = I18N_LANGUAGES.filter((lang) => { + return ( + Object.keys(record?.translations).includes(lang.value) && + record?.translations[lang.value] !== null + ); + }); + // Appends English by default + items.unshift(I18N_LANGUAGES[0]); + return items; + }; + + const handleAnnounceDownload = async (record, lang) => { + let response = await getAnnounceDoc(record.id, lang); + if (!response) { + notification.error({ + message: "ERROR", + description: "Couldn't download file", + }); + return; + } + downloadBlob(response, record.file_name); + }; + + const columns = [ + { + title: "Name", + dataIndex: "name", + key: "name", + render: (name) => <>{name}, + }, + { + title: "Description", + dataIndex: "description", + key: "description", + render: (description) => <>{description}, + }, + { + title: "Document", + dataIndex: "file_name", + key: "file_name", + render: (file_name, record) => { + if (file_name && file_name !== "") { + return ( + handleAnnounceDownload(record, lang)} + /> + ); + } + }, + }, + { + title: "Image", + dataIndex: "image", + key: "image", + render: (image) => { + if (image) { + return ; + } + }, + }, + { + title: "", + dataIndex: "id", + key: "id", + render: (id) => ( + + + + ), + align: "center", + }, + ]; + + // Add some kind of error 403 code + return ( + <> + +
+ +
+
+ { + setMobileFilterVisible(false); + }} + open={mobileFilterVisible} + footer={[ + , + , + ]} + bodyStyle={{ + padding: "1rem", + }} + > + {getFilterForm()} + +
+ + +
+ {getFilterForm()} +
+
+ + {!pageLoaded ? ( +
+ +
+ ) : ( +
+
+ + )} + + + ); +} + +export default Announcements; diff --git a/frontend/src/components/pages/HomeLayout.js b/frontend/src/components/pages/HomeLayout.js index 4032e630..b6cd8494 100644 --- a/frontend/src/components/pages/HomeLayout.js +++ b/frontend/src/components/pages/HomeLayout.js @@ -128,17 +128,52 @@ function HomeLayout({ children, ignoreHomeLayout, allHubData, location }) { `} onClick={() => history.push("/")} /> */} - {(isTablet && N50Path.includes(location.pathname)) ? ( - {""} + {isTablet && N50Path.includes(location.pathname) ? ( + <> + {""} +
+
+
+ {t("common.powered_by")} +
+
+ {""} history.push("/")} + /> +
+
+
+ ) : (
{ export const getTrainings = async ( role, user_email = null, + user_id = null, lang = i18n.language ) => { const requestExtension = `/training/${role}`; @@ -271,6 +272,7 @@ export const getTrainings = async ( params: { lang: lang, user_email: user_email, + user_id: user_id, }, }).catch(console.error); const trains = res.data.result.trainings; @@ -368,6 +370,97 @@ export const newPolicyCreate = async (values) => { return response?.data; }; +export const getAnnouncements = async ( + role, + user_id = null, + hub_user_id = null, + lang = i18n.language +) => { + const requestExtension = `/announcement/${role}`; + const res = await authGet(requestExtension, { + params: { + lang: lang, + user_id: user_id, + hub_user_id: hub_user_id, + }, + }).catch(console.error); + const data = res.data.result.res; + let newData = []; + for (let item of data) { + item.id = item._id["$oid"]; + newData.push(item); + } + return newData; +}; + +export const newAnnounceCreate = async (values) => { + const { role } = values; + const requestExtension = `/announcement/register/${role}`; + const formData = new FormData(); + formData.append("front_url", FRONT_BASE_URL); + Object.entries(values).forEach(([key, value]) => { + formData.append(key, value); + }); + let response = await authPost(requestExtension, formData).catch((err) => { + console.error(err); + }); + return response?.data; +}; + +export const uploadAnnounceImage = (image, id) => { + const requestExtension = `/announcement/upload/${id}/image`; + let formData = new FormData(); + formData.append("image", image); + return authPut(requestExtension, formData).then( + (response) => response, + (err) => { + console.error(err); + } + ); +}; + +export const getAnnounceDoc = async (id, lang = i18n.language) => { + const requestExtension = `/announcement/getDoc/${id}`; + let response = await authGet(requestExtension, { + responseType: "blob", + params: { + lang: lang, + }, + }).catch(console.error); + return response; +}; + +export const deleteAnnouncebyId = (id) => { + const requestExtension = `/announcement/delete/${id}`; + return authDelete(requestExtension).then( + (response) => response, + (err) => { + console.error(err); + return false; + } + ); +}; + +export const getAnnounceById = async (id) => { + const requestExtension = `/announcement/get/${id}`; + let response = await authGet(requestExtension, { + params: {}, + }).catch(console.error); + const announcement = response.data.result.announcement; + return announcement; +}; +export const EditAnnounceById = async (id, values = []) => { + const requestExtension = `/announcement/edit/${id}`; + const formData = new FormData(); + Object.entries(values).forEach(([key, value]) => { + formData.append(key, value); + }); + let response = await authPut(requestExtension, formData).catch((err) => { + console.error(err); + }); + return response?.data; +}; + export const newTrainCreate = async (values) => { const { role } = values; const requestExtension = `/training/${role}`; diff --git a/frontend/src/utils/hooks/useSidebars.js b/frontend/src/utils/hooks/useSidebars.js index c42b6428..3ac0902f 100644 --- a/frontend/src/utils/hooks/useSidebars.js +++ b/frontend/src/utils/hooks/useSidebars.js @@ -75,6 +75,11 @@ export default function useSidebars(userType, user, t) { key: "events", icon: , }, + { + label: t("sidebars.announcements"), + key: "announcements", + icon: , + }, { label: t("sidebars.videos"), key: "videos", @@ -123,6 +128,11 @@ export default function useSidebars(userType, user, t) { key: "events", icon: , }, + { + label: t("sidebars.announcements"), + key: "announcements", + icon: , + }, { label: t("sidebars.appointments"), key: "mentee-appointments", @@ -172,6 +182,11 @@ export default function useSidebars(userType, user, t) { key: "events", icon: , }, + { + label: t("sidebars.announcements"), + key: "announcements", + icon: , + }, { label: t("sidebars.profile"), key: "profile", @@ -355,6 +370,11 @@ export default function useSidebars(userType, user, t) { key: "admin-training", icon: , }, + { + label: "Announcement", + key: "admin-announcement", + icon: , + }, { label: "Sign Docs", key: "admin-sign",