diff --git a/backend/api/models/MenteeProfile.py b/backend/api/models/MenteeProfile.py index f57be4dd3..13d3bf555 100644 --- a/backend/api/models/MenteeProfile.py +++ b/backend/api/models/MenteeProfile.py @@ -33,6 +33,7 @@ class MenteeProfile(Document, Mixin): workstate = ListField(StringField(), required=False) preferred_language = StringField(required=False, default="en-US") roomName = StringField(required=False) + timezone = StringField(required=True) def __repr__(self): return f""" 0 and mentee.email_notifications: res, res_msg = ( send_email( @@ -176,6 +195,24 @@ def create_appointment(): date_object = datetime.strptime(time_data.get("start_time"), "%Y-%m-%dT%H:%M:%S%z") start_time = date_object.strftime(APPT_TIME_FORMAT + " %Z") + if mentee.timezone: + print("timezone", mentee.timezone) + match = re.match(r"UTC([+-]\d{2}):(\d{2})", mentee.timezone) + print("match", match) + if match: + hours_offset = int(match.group(1)) + minutes_offset = int(match.group(2)) + # Create a timezone with the parsed offset + offset = timezone(timedelta(hours=hours_offset, minutes=minutes_offset)) + # Convert the datetime to the target timezone + converted_date = date_object.astimezone(offset) + start_time_local_timezone = converted_date.strftime( + APPT_TIME_FORMAT + " %Z" + ) + else: + start_time_local_timezone = start_time + else: + start_time_local_timezone = start_time if mentee.email_notifications: res, res_msg = send_email( @@ -184,7 +221,7 @@ def create_appointment(): data={ "confirmation": True, "name": mentor.name, - "date": start_time, + "date": start_time_local_timezone, mentee.preferred_language: True, "subject": TRANSLATIONS[mentee.preferred_language]["mentee_appt"], }, @@ -194,12 +231,31 @@ def create_appointment(): logger.info(msg) if mentor.email_notifications: + if mentor.timezone: + print("timezone", mentor.timezone) + match = re.match(r"UTC([+-]\d{2}):(\d{2})", mentor.timezone) + print("match", match) + if match: + hours_offset = int(match.group(1)) + minutes_offset = int(match.group(2)) + # Create a timezone with the parsed offset + offset = timezone(timedelta(hours=hours_offset, minutes=minutes_offset)) + # Convert the datetime to the target timezone + converted_date = date_object.astimezone(offset) + start_time_local_timezone = converted_date.strftime( + APPT_TIME_FORMAT + " %Z" + ) + else: + start_time_local_timezone = start_time + else: + start_time_local_timezone = start_time + res, res_msg = send_email( recipient=mentor.email, template_id=MENTOR_APPT_TEMPLATE, data={ "name": mentee.name, - "date": start_time, + "date": start_time_local_timezone, mentor.preferred_language: True, "subject": TRANSLATIONS[mentee.preferred_language]["mentor_appt"], }, @@ -247,11 +303,30 @@ def put_appointment(id): if mentee.email_notifications: start_time = appointment.timeslot.start_time.strftime(APPT_TIME_FORMAT + " GMT") + if mentee.timezone: + print("timezone", mentee.timezone) + match = re.match(r"UTC([+-]\d{2}):(\d{2})", mentee.timezone) + print("match", match) + if match: + hours_offset = int(match.group(1)) + minutes_offset = int(match.group(2)) + # Create a timezone with the parsed offset + offset = timezone(timedelta(hours=hours_offset, minutes=minutes_offset)) + # Convert the datetime to the target timezone + converted_date = appointment.timeslot.start_time.astimezone(offset) + start_time_local_timezone = converted_date.strftime( + APPT_TIME_FORMAT + " %Z" + ) + else: + start_time_local_timezone = start_time + else: + start_time_local_timezone = start_time + res_email = send_email( recipient=mentee.email, data={ "name": mentor.name, - "date": start_time, + "date": start_time_local_timezone, "approved": True, mentee.preferred_language: True, "subject": TRANSLATIONS[mentee.preferred_language]["mentee_appt"], @@ -282,6 +357,26 @@ def delete_request(appointment_id): if mentee.email_notifications: start_time = request.timeslot.start_time.strftime(f"{APPT_TIME_FORMAT} GMT") + + if mentee.timezone: + print("timezone", mentee.timezone) + match = re.match(r"UTC([+-]\d{2}):(\d{2})", mentee.timezone) + print("match", match) + if match: + hours_offset = int(match.group(1)) + minutes_offset = int(match.group(2)) + # Create a timezone with the parsed offset + offset = timezone(timedelta(hours=hours_offset, minutes=minutes_offset)) + # Convert the datetime to the target timezone + converted_date = request.timeslot.start_time.astimezone(offset) + start_time_local_timezone = converted_date.strftime( + APPT_TIME_FORMAT + " %Z" + ) + else: + start_time_local_timezone = start_time + else: + start_time_local_timezone = start_time + res_email = send_email( recipient=mentee.email, data={ diff --git a/backend/api/views/events.py b/backend/api/views/events.py index 959e1a7de..1543fcd61 100644 --- a/backend/api/views/events.py +++ b/backend/api/views/events.py @@ -11,11 +11,11 @@ MenteeProfile, PartnerProfile, ) -from datetime import datetime +from datetime import datetime, timezone, timedelta +import re from api.utils.translate import ( get_all_translations, ) -from datetime import datetime from api.utils.constants import ( Account, EVENT_TEMPLATE, @@ -77,8 +77,26 @@ def delete_train(id): ###################################################################### -def send_mail_for_event(recipients, role_name, title, eventdate, target_url): +def send_mail_for_event( + recipients, role_name, title, eventdate, target_url, start_datetime, end_datetime +): for recipient in recipients: + if recipient.timezone: + print("timezone", recipient.timezone) + match = re.match(r"UTC([+-]\d{2}):(\d{2})", recipient.timezone) + print("match", match) + if match: + hours_offset = int(match.group(1)) + minutes_offset = int(match.group(2)) + # Create a timezone with the parsed offset + offset = timezone(timedelta(hours=hours_offset, minutes=minutes_offset)) + # Convert the datetime to the target timezone + eventdate.append( + start_datetime.astimezone(offset).strftime("%m-%d-%Y %I:%M%p %Z") + + " ~ " + + end_datetime.astimezone(offset).strftime("%m-%d-%Y %I:%M%p %Z") + ) + res, res_msg = send_email( recipient=recipient.email, data={ @@ -151,18 +169,48 @@ def new_event(): if start_datetime_str != "": eventdate = start_datetime_str + " ~ " + end_datetime_str if Account.MENTOR in roles: - recipients = MentorProfile.objects.only("email", "preferred_language") + recipients = MentorProfile.objects.only( + "email", "preferred_language", "timezone" + ) role_name = "MENTOR" - send_mail_for_event(recipients, role_name, title, eventdate, target_url) + send_mail_for_event( + recipients, + role_name, + title, + eventdate, + target_url, + start_datetime, + end_datetime, + ) if Account.MENTEE in roles: - recipients = MenteeProfile.objects.only("email", "preferred_language") + recipients = MenteeProfile.objects.only( + "email", "preferred_language", "timezone" + ) role_name = "MENTEE" - send_mail_for_event(recipients, role_name, title, eventdate, target_url) + send_mail_for_event( + recipients, + role_name, + title, + eventdate, + target_url, + start_datetime, + end_datetime, + ) if Account.PARTNER in roles: - recipients = PartnerProfile.objects.only("email", "preferred_language") + recipients = PartnerProfile.objects.only( + "email", "preferred_language", "timezone" + ) role_name = "Partner" - send_mail_for_event(recipients, role_name, title, eventdate, target_url) + send_mail_for_event( + recipients, + role_name, + title, + eventdate, + target_url, + start_datetime, + end_datetime, + ) if hub_id is not None: role_name = "Hub" partners = PartnerProfile.objects.filter(hub_id=hub_id).only( diff --git a/frontend/public/locales/ar/translation.json b/frontend/public/locales/ar/translation.json index a27be7d19..4d73740c3 100644 --- a/frontend/public/locales/ar/translation.json +++ b/frontend/public/locales/ar/translation.json @@ -165,7 +165,10 @@ "requiredCheckbox":"يرجى التحقق من هذا الحقل", "requiredPartner": "الرجاء اختيار شريك", "stopImpersonating":"التوقف عن انتحال الشخصية", - "impersonate":"انتحال شخصية" + "impersonate":"انتحال شخصية", + "timezone":"المنطقة الزمنية", + "requiredTimezone":"الرجاء إدخال المنطقة الزمنية", + "requiredAvatar":"الرجاء تحميل صورة الملف الشخصي" }, "commonApplication": { "no-affiliation":"عدم الانتماء", diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index 52caf9f96..5d77e8b5e 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -166,7 +166,10 @@ "requiredCheckbox":"Please check this field", "requiredPartner": "Please select a Partner", "stopImpersonating":"Stop Impersonating", - "impersonate":"Impersonate" + "impersonate":"Impersonate", + "timezone":"Time Zone", + "requiredTimezone":"Please enter time zone", + "requiredAvatar":"Please upload profile image" }, "commonApplication": { "no-affiliation":"no-affiliation", diff --git a/frontend/public/locales/es-US/translation.json b/frontend/public/locales/es-US/translation.json index 17b8be3bf..d2700cc90 100644 --- a/frontend/public/locales/es-US/translation.json +++ b/frontend/public/locales/es-US/translation.json @@ -165,7 +165,10 @@ "requiredCheckbox":"Por favor revisa este campo", "requiredPartner": "Por favor seleccione un socio", "stopImpersonating":"Deja de hacerte pasar por", - "impersonate":"Personificar" + "impersonate":"Personificar", + "timezone":"Huso horario", + "requiredTimezone":"Por favor, introduzca la zona horaria", + "requiredAvatar":"Por favor sube una imagen de perfil" }, "commonApplication": { "no-affiliation":"sin afiliación", diff --git a/frontend/public/locales/fa-AF/translation.json b/frontend/public/locales/fa-AF/translation.json index 731a93f49..31c285f9b 100644 --- a/frontend/public/locales/fa-AF/translation.json +++ b/frontend/public/locales/fa-AF/translation.json @@ -165,7 +165,10 @@ "requiredCheckbox":"لطفا این فیلد را بررسی کنید", "requiredPartner": "لطفا یک شریک انتخاب کنید", "stopImpersonating":"جعل هویت را متوقف کنید", - "impersonate":"جعل هویت" + "impersonate":"جعل هویت", + "timezone":"منطقه زمانی", + "requiredTimezone":"لطفا منطقه زمانی را وارد کنید", + "requiredAvatar":"لطفا تصویر نمایه را آپلود کنید" }, "commonApplication": { "no-affiliation":"عدم وابستگی", diff --git a/frontend/public/locales/pa-AR/translation.json b/frontend/public/locales/pa-AR/translation.json index fad8c2a63..f5788ef02 100644 --- a/frontend/public/locales/pa-AR/translation.json +++ b/frontend/public/locales/pa-AR/translation.json @@ -166,7 +166,10 @@ "requiredCheckbox":"مهرباني وکړئ دا ساحه وګورئ", "requiredPartner": "مهرباني وکړئ یو شریک وټاکئ", "stopImpersonating":"تقلید بند کړئ", - "impersonate":"تقلید" + "impersonate":"تقلید", + "timezone":"د وخت زون", + "requiredTimezone":"مهرباني وکړئ د وخت زون دننه کړئ", + "requiredAvatar":"مهرباني وکړئ د پروفایل عکس اپلوډ کړئ" }, "commonApplication": { "no-affiliation":"بې تړاو", diff --git a/frontend/public/locales/pt-BR/translation.json b/frontend/public/locales/pt-BR/translation.json index 7c5771758..68f04dc31 100644 --- a/frontend/public/locales/pt-BR/translation.json +++ b/frontend/public/locales/pt-BR/translation.json @@ -165,7 +165,10 @@ "requiredCheckbox":"Por favor verifique este campo", "requiredPartner": "Selecione um parceiro", "stopImpersonating":"Pare de se passar por", - "impersonate":"Personificar" + "impersonate":"Personificar", + "timezone":"Fuso horário", + "requiredTimezone":"Por favor, insira o fuso horário", + "requiredAvatar":"Por favor, carregue a imagem do perfil" }, "commonApplication": { "no-affiliation":"sem afiliação", diff --git a/frontend/src/components/NavigationHeader.js b/frontend/src/components/NavigationHeader.js index d04aa7625..410c004bc 100644 --- a/frontend/src/components/NavigationHeader.js +++ b/frontend/src/components/NavigationHeader.js @@ -19,6 +19,7 @@ import { } from "@ant-design/icons"; import { fetchUser } from "features/userSlice"; import { uploadAccountImage } from "utils/api"; +import Profile from "./pages/Profile"; const { Header } = Layout; @@ -155,6 +156,9 @@ function NavigationHeader() { style={{ background: colorBgContainer, display: "flex" }} theme="light" > + {!user.timezone && ( +
Please set time zone in profile page
+ )} {isMobile && dispatch(collapse())} />}
{supportUserID && user && ( diff --git a/frontend/src/components/PartnerProfileForm.js b/frontend/src/components/PartnerProfileForm.js index c5d662e65..c9e633896 100644 --- a/frontend/src/components/PartnerProfileForm.js +++ b/frontend/src/components/PartnerProfileForm.js @@ -2,7 +2,12 @@ import React, { useEffect, useState } from "react"; import { withRouter } from "react-router-dom"; import { Button, Upload, Avatar, Form, Select, Input, Radio } from "antd"; import { useTranslation } from "react-i18next"; -import { ACCOUNT_TYPE, getRegions, getSDGs } from "../utils/consts"; +import { + ACCOUNT_TYPE, + getRegions, + getSDGs, + TIMEZONE_OPTIONS, +} from "../utils/consts"; import { urlRegex } from "../utils/misc"; import ImgCrop from "antd-img-crop"; import { UserOutlined, EditFilled } from "@ant-design/icons"; @@ -38,6 +43,7 @@ function PartnerProfileForm({ const [image, setImage] = useState(null); const [changedImage, setChangedImage] = useState(false); const [edited, setEdited] = useState(false); + const [finishFlag, setFinishFlag] = useState(false); const [form] = Form.useForm(); useEffect(() => { @@ -49,6 +55,8 @@ function PartnerProfileForm({ }, [profileData, form, resetFields]); const onFinish = async (values) => { + setFinishFlag(true); + if (!image) return; let newData = values; if (email) { newData.email = email; @@ -105,6 +113,16 @@ function PartnerProfileForm({ /> + {!image && finishFlag && ( + + )} {hub_user && (
- + + - - - - - -
- - - -
- {key !== 0 && ( - remove(name)} - className={css` - float: right; - color: #ff4d4f; - `} - /> - )} - - - ))} - - - - - )} - - ); - const onFinish = async (values) => { + setFinishFlag(true); + if (!image) return; let newData = values; newData.email = email; newData.role = ACCOUNT_TYPE.MENTEE; @@ -380,7 +269,25 @@ function MenteeProfileForm({ /> + {!image && finishFlag && ( + + )} + @@ -524,6 +431,19 @@ function MenteeProfileForm({ + { async function getPartners() { @@ -215,6 +220,8 @@ function MentorProfileForm({ ); const onFinish = async (values) => { + setFinishFlag(true); + if (!image) return; let newData = values; newData.email = email; newData.role = ACCOUNT_TYPE.MENTOR; @@ -273,6 +280,16 @@ function MentorProfileForm({ /> + {!image && finishFlag && ( + + )}
+ {t("commonProfile.education")} diff --git a/frontend/src/utils/consts.js b/frontend/src/utils/consts.js index e9a45efd1..627a4b155 100644 --- a/frontend/src/utils/consts.js +++ b/frontend/src/utils/consts.js @@ -228,3 +228,48 @@ export const I18N_LANGUAGES = [ { value: "fa-AF", label: "Farsi" }, { value: "pa-AR", label: "Pashto" }, ]; + +export const TIMEZONE_OPTIONS = [ + { value: "UTC-12:00", label: "(UTC-12:00) International Date Line West" }, + { value: "UTC-11:00", label: "(UTC-11:00) Coordinated Universal Time-11" }, + { value: "UTC-10:00", label: "(UTC-10:00) Hawaii" }, + { value: "UTC-09:00", label: "(UTC-09:00) Alaska" }, + { value: "UTC-08:00", label: "(UTC-08:00) Pacific Time (US & Canada)" }, + { value: "UTC-07:00", label: "(UTC-07:00) Mountain Time (US & Canada)" }, + { value: "UTC-06:00", label: "(UTC-06:00) Central Time (US & Canada)" }, + { value: "UTC-05:00", label: "(UTC-05:00) Eastern Time (US & Canada)" }, + { value: "UTC-04:00", label: "(UTC-04:00) Atlantic Time (Canada)" }, + { value: "UTC-03:00", label: "(UTC-03:00) Buenos Aires" }, + { value: "UTC-02:00", label: "(UTC-02:00) Coordinated Universal Time-02" }, + { value: "UTC-01:00", label: "(UTC-01:00) Azores" }, + { + value: "UTC+00:00", + label: "(UTC+00:00) Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London", + }, + { + value: "UTC+01:00", + label: "(UTC+01:00) Central European Time: Amsterdam, Berlin, Rome, Paris", + }, + { + value: "UTC+02:00", + label: "(UTC+02:00) Eastern European Time: Athens, Bucharest, Jerusalem", + }, + { + value: "UTC+03:00", + label: "(UTC+03:00) Moscow, St. Petersburg, Volgograd", + }, + { value: "UTC+04:00", label: "(UTC+04:00) Abu Dhabi, Muscat" }, + { value: "UTC+05:00", label: "(UTC+05:00) Islamabad, Karachi" }, + { value: "UTC+06:00", label: "(UTC+06:00) Dhaka" }, + { value: "UTC+07:00", label: "(UTC+07:00) Bangkok, Hanoi, Jakarta" }, + { + value: "UTC+08:00", + label: "(UTC+08:00) Beijing, Chongqing, Hong Kong, Urumqi", + }, + { value: "UTC+09:00", label: "(UTC+09:00) Tokyo, Osaka, Sapporo" }, + { value: "UTC+10:00", label: "(UTC+10:00) Sydney, Melbourne, Brisbane" }, + { value: "UTC+11:00", label: "(UTC+11:00) Solomon Islands, New Caledonia" }, + { value: "UTC+12:00", label: "(UTC+12:00) Auckland, Wellington" }, + { value: "UTC+13:00", label: "(UTC+13:00) Nuku'alofa" }, + { value: "UTC+14:00", label: "(UTC+14:00) Kiritimati Island" }, +];